@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.
- package/README.md +148 -0
- package/auto-deploy.js +190 -0
- package/bin/aiva.js +81 -0
- package/cli-sync.js +126 -0
- package/d2a-prompt-template.txt +106 -0
- package/diagnostics-api.js +304 -0
- package/docs/ara-dedup-fix-scope.md +112 -0
- package/docs/ara-fix-round2-scope.md +61 -0
- package/docs/ara-greeting-fix-scope.md +70 -0
- package/docs/calendar-date-fix-scope.md +28 -0
- package/docs/getting-started.md +115 -0
- package/docs/network-architecture-rollout-scope.md +43 -0
- package/docs/scope-google-oauth-integration.md +351 -0
- package/docs/settings-page-scope.md +50 -0
- package/docs/xai-imagine-scope.md +116 -0
- package/docs/xai-voice-integration-scope.md +115 -0
- package/docs/xai-voice-tools-scope.md +165 -0
- package/email-router.js +512 -0
- package/follow-up-handler.js +606 -0
- package/gateway-monitor.js +158 -0
- package/google-email.js +379 -0
- package/google-oauth.js +310 -0
- package/grok-imagine.js +97 -0
- package/health-reporter.js +287 -0
- package/invisible-prefix-base.txt +206 -0
- package/invisible-prefix-owner.txt +26 -0
- package/invisible-prefix-slim.txt +10 -0
- package/invisible-prefix.txt +43 -0
- package/knowledge-base.js +472 -0
- package/lib/cli.js +19 -0
- package/lib/config.js +124 -0
- package/lib/health.js +57 -0
- package/lib/process.js +207 -0
- package/lib/server.js +42 -0
- package/lib/setup.js +472 -0
- package/meta-capi.js +206 -0
- package/meta-leads.js +411 -0
- package/notion-oauth.js +323 -0
- package/package.json +61 -0
- package/public/agent-config.html +241 -0
- package/public/aiva-avatar-anime.png +0 -0
- package/public/css/docs.css.bak +688 -0
- package/public/css/onboarding.css +543 -0
- package/public/diagrams/claude-subscription-pool.html +329 -0
- package/public/diagrams/claude-subscription-pool.png +0 -0
- package/public/docs-icon.png +0 -0
- package/public/escalation.html +237 -0
- package/public/group-config.html +300 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icons/agents.svg +1 -0
- package/public/icons/attach.svg +1 -0
- package/public/icons/characters.svg +1 -0
- package/public/icons/chat.svg +1 -0
- package/public/icons/docs.svg +1 -0
- package/public/icons/heartbeat.svg +1 -0
- package/public/icons/messages.svg +1 -0
- package/public/icons/mic.svg +1 -0
- package/public/icons/notes.svg +1 -0
- package/public/icons/settings.svg +1 -0
- package/public/icons/tasks.svg +1 -0
- package/public/images/onboarding/p0-communication-layer.png +0 -0
- package/public/images/onboarding/p0-infinite-surface.png +0 -0
- package/public/images/onboarding/p0-learning-model.png +0 -0
- package/public/images/onboarding/p0-meet-aiva.png +0 -0
- package/public/images/onboarding/p4-contact-intelligence.png +0 -0
- package/public/images/onboarding/p4-context-compounds.png +0 -0
- package/public/images/onboarding/p4-message-router.png +0 -0
- package/public/images/onboarding/p4-per-contact-rules.png +0 -0
- package/public/images/onboarding/p4-send-messages.png +0 -0
- package/public/images/onboarding/p6-be-precise.png +0 -0
- package/public/images/onboarding/p6-review-escalations.png +0 -0
- package/public/images/onboarding/p6-voice-input.png +0 -0
- package/public/images/onboarding/p7-completion.png +0 -0
- package/public/index.html +11594 -0
- package/public/js/onboarding.js +699 -0
- package/public/manifest.json +24 -0
- package/public/messages-v2.html +2824 -0
- package/public/permission-approve.html.bak +107 -0
- package/public/permissions.html +150 -0
- package/public/styles/design-system.css +68 -0
- package/router-db.js +604 -0
- package/router-utils.js +28 -0
- package/router-v2/adapters/imessage.js +191 -0
- package/router-v2/adapters/quo.js +82 -0
- package/router-v2/adapters/whatsapp.js +192 -0
- package/router-v2/contact-manager.js +234 -0
- package/router-v2/conversation-engine.js +498 -0
- package/router-v2/data/knowledge-base.json +176 -0
- package/router-v2/data/router-v2.db +0 -0
- package/router-v2/data/router-v2.db-shm +0 -0
- package/router-v2/data/router-v2.db-wal +0 -0
- package/router-v2/data/router.db +0 -0
- package/router-v2/db.js +457 -0
- package/router-v2/escalation-bridge.js +540 -0
- package/router-v2/follow-up-engine.js +347 -0
- package/router-v2/index.js +441 -0
- package/router-v2/ingestion.js +213 -0
- package/router-v2/knowledge-base.js +231 -0
- package/router-v2/lead-qualifier.js +152 -0
- package/router-v2/learning-loop.js +202 -0
- package/router-v2/outbound-sender.js +160 -0
- package/router-v2/package.json +13 -0
- package/router-v2/permission-gate.js +86 -0
- package/router-v2/playbook.js +177 -0
- package/router-v2/prompts/base.js +52 -0
- package/router-v2/prompts/first-contact.js +38 -0
- package/router-v2/prompts/lead-qualification.js +37 -0
- package/router-v2/prompts/scheduling.js +72 -0
- package/router-v2/prompts/style-overrides.js +22 -0
- package/router-v2/scheduler.js +301 -0
- package/router-v2/scripts/migrate-v1-to-v2.js +215 -0
- package/router-v2/scripts/seed-faq.js +67 -0
- package/router-v2/seed-knowledge-base.js +39 -0
- package/router-v2/utils/ai.js +129 -0
- package/router-v2/utils/phone.js +52 -0
- package/router-v2/utils/response-validator.js +98 -0
- package/router-v2/utils/sanitize.js +222 -0
- package/router.js +5005 -0
- package/routes/google-calendar.js +186 -0
- package/scripts/deploy.sh +62 -0
- package/scripts/macos-calendar.sh +232 -0
- package/scripts/onboard-device.sh +466 -0
- package/server.js +5131 -0
- package/start.sh +24 -0
- package/templates/AGENTS.md +548 -0
- package/templates/IDENTITY.md +15 -0
- package/templates/docs-agents.html +132 -0
- package/templates/docs-app.html +130 -0
- package/templates/docs-home.html +83 -0
- package/templates/docs-imessage.html +121 -0
- package/templates/docs-tasks.html +123 -0
- package/templates/docs-tips.html +175 -0
- package/templates/getting-started.html +809 -0
- package/templates/invisible-prefix-base.txt +171 -0
- package/templates/invisible-prefix-owner.txt +282 -0
- package/templates/invisible-prefix.txt +338 -0
- package/templates/manifest.json +61 -0
- package/templates/memory-org/clients.md +7 -0
- package/templates/memory-org/credentials.md +9 -0
- package/templates/memory-org/devices.md +7 -0
- package/templates/updates.html +464 -0
- package/templates/workspace/AGENTS.md.tmpl +161 -0
- package/templates/workspace/HEARTBEAT.md.tmpl +17 -0
- package/templates/workspace/IDENTITY.md.tmpl +15 -0
- package/templates/workspace/MEMORY.md.tmpl +16 -0
- package/templates/workspace/SOUL.md.tmpl +51 -0
- package/templates/workspace/USER.md.tmpl +25 -0
- package/tts-proxy.js +96 -0
- package/voice-call-local.js +731 -0
- package/voice-call.js +732 -0
- package/wa-listener.js +354 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIVA Remote Diagnostics API
|
|
3
|
+
* Port 3850 — exposes device health, logs, errors, system info
|
|
4
|
+
* Authenticated via DIAG_TOKEN (Bearer token), except /health
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
const url = require('url');
|
|
13
|
+
|
|
14
|
+
const PORT = 3850;
|
|
15
|
+
const DIAG_TOKEN = process.env.DIAG_TOKEN || '5f734d1d83211f8fff8f21b53e1c42d9bc27ad7bd47ae1ebc9b72fd9ef3c9281';
|
|
16
|
+
const HOME = os.homedir();
|
|
17
|
+
const AIVA_DIR = path.join(HOME, '.openclaw', 'workspace', 'aiva-tasks');
|
|
18
|
+
const startTime = Date.now();
|
|
19
|
+
|
|
20
|
+
// --- Helpers ---
|
|
21
|
+
|
|
22
|
+
function readLastLines(filePath, n = 100) {
|
|
23
|
+
try {
|
|
24
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
25
|
+
const lines = content.split('\n');
|
|
26
|
+
return lines.slice(-n).join('\n');
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sendJSON(res, statusCode, data) {
|
|
33
|
+
res.writeHead(statusCode, {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'Access-Control-Allow-Origin': '*',
|
|
36
|
+
'Access-Control-Allow-Headers': 'Authorization, Content-Type',
|
|
37
|
+
});
|
|
38
|
+
res.end(JSON.stringify(data));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function authenticate(req) {
|
|
42
|
+
const auth = req.headers['authorization'];
|
|
43
|
+
if (!auth || !auth.startsWith('Bearer ')) return false;
|
|
44
|
+
return auth.slice(7) === DIAG_TOKEN;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeExec(cmd) {
|
|
48
|
+
try {
|
|
49
|
+
return execSync(cmd, { encoding: 'utf8', timeout: 10000 }).trim();
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Route Handlers ---
|
|
56
|
+
|
|
57
|
+
function handleHealth(req, res) {
|
|
58
|
+
sendJSON(res, 200, {
|
|
59
|
+
status: 'ok',
|
|
60
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
61
|
+
hostname: os.hostname(),
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleLogs(req, res, query) {
|
|
67
|
+
const lines = parseInt(query.lines) || 100;
|
|
68
|
+
const type = query.type || 'out'; // out | err
|
|
69
|
+
|
|
70
|
+
// Try PM2 logs first
|
|
71
|
+
const pm2LogDir = path.join(HOME, '.pm2', 'logs');
|
|
72
|
+
const suffix = type === 'err' ? 'error' : 'out';
|
|
73
|
+
const pm2File = path.join(pm2LogDir, `aiva-app-${suffix}.log`);
|
|
74
|
+
|
|
75
|
+
let content = readLastLines(pm2File, lines);
|
|
76
|
+
let source = 'pm2';
|
|
77
|
+
|
|
78
|
+
if (!content) {
|
|
79
|
+
// Try LaunchAgent logs
|
|
80
|
+
const laFile = type === 'err'
|
|
81
|
+
? path.join(HOME, 'Library', 'Logs', 'aiva-app-stderr.log')
|
|
82
|
+
: path.join(HOME, 'Library', 'Logs', 'aiva-app-stdout.log');
|
|
83
|
+
content = readLastLines(laFile, lines);
|
|
84
|
+
source = 'launchagent';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sendJSON(res, 200, {
|
|
88
|
+
source,
|
|
89
|
+
type,
|
|
90
|
+
lines: lines,
|
|
91
|
+
content: content || 'No log file found',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function handleErrors(req, res) {
|
|
96
|
+
// Parse PM2 error log and out log for error patterns
|
|
97
|
+
const logFiles = [
|
|
98
|
+
path.join(HOME, '.pm2', 'logs', 'aiva-app-error.log'),
|
|
99
|
+
path.join(HOME, '.pm2', 'logs', 'aiva-app-out.log'),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const errorPattern = /error|Error|ERROR|TypeError|ReferenceError|SyntaxError|ENOENT|ECONNREFUSED|UnhandledPromise|stack trace/i;
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const buckets = { '1h': 0, '6h': 0, '24h': 0 };
|
|
105
|
+
const recentErrors = [];
|
|
106
|
+
|
|
107
|
+
for (const file of logFiles) {
|
|
108
|
+
let content;
|
|
109
|
+
try {
|
|
110
|
+
content = fs.readFileSync(file, 'utf8');
|
|
111
|
+
} catch { continue; }
|
|
112
|
+
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
if (!errorPattern.test(line)) continue;
|
|
116
|
+
|
|
117
|
+
// Try to extract timestamp - PM2 logs often have ISO timestamps
|
|
118
|
+
const tsMatch = line.match(/(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})/);
|
|
119
|
+
if (tsMatch) {
|
|
120
|
+
const ts = new Date(tsMatch[1]).getTime();
|
|
121
|
+
if (isNaN(ts)) continue;
|
|
122
|
+
const age = now - ts;
|
|
123
|
+
if (age < 3600000) buckets['1h']++;
|
|
124
|
+
if (age < 21600000) buckets['6h']++;
|
|
125
|
+
if (age < 86400000) buckets['24h']++;
|
|
126
|
+
if (age < 3600000 && recentErrors.length < 20) {
|
|
127
|
+
recentErrors.push(line.trim().slice(0, 200));
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// No timestamp - count in 24h bucket
|
|
131
|
+
buckets['24h']++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
sendJSON(res, 200, { buckets, recentErrors });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function handleSystem(req, res) {
|
|
140
|
+
const cpus = os.cpus();
|
|
141
|
+
const loadAvg = os.loadavg();
|
|
142
|
+
const totalMem = os.totalmem();
|
|
143
|
+
const freeMem = os.freemem();
|
|
144
|
+
|
|
145
|
+
// macOS version
|
|
146
|
+
const macVersion = safeExec('sw_vers -productVersion') || 'unknown';
|
|
147
|
+
const nodeVersion = process.version;
|
|
148
|
+
|
|
149
|
+
// Disk usage
|
|
150
|
+
let disk = null;
|
|
151
|
+
const dfOut = safeExec("df -h / | tail -1");
|
|
152
|
+
if (dfOut) {
|
|
153
|
+
const parts = dfOut.split(/\s+/);
|
|
154
|
+
disk = { total: parts[1], used: parts[2], available: parts[3], pctUsed: parts[4] };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Tailscale IP
|
|
158
|
+
const tailscaleIP = safeExec('tailscale ip -4') || null;
|
|
159
|
+
|
|
160
|
+
// Network interfaces (simplified)
|
|
161
|
+
const nets = os.networkInterfaces();
|
|
162
|
+
const interfaces = {};
|
|
163
|
+
for (const [name, addrs] of Object.entries(nets)) {
|
|
164
|
+
interfaces[name] = addrs
|
|
165
|
+
.filter(a => !a.internal)
|
|
166
|
+
.map(a => ({ address: a.address, family: a.family }));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
sendJSON(res, 200, {
|
|
170
|
+
hostname: os.hostname(),
|
|
171
|
+
platform: os.platform(),
|
|
172
|
+
arch: os.arch(),
|
|
173
|
+
macVersion,
|
|
174
|
+
nodeVersion,
|
|
175
|
+
uptime: os.uptime(),
|
|
176
|
+
cpuCount: cpus.length,
|
|
177
|
+
cpuModel: cpus[0]?.model,
|
|
178
|
+
loadAvg: { '1m': loadAvg[0], '5m': loadAvg[1], '15m': loadAvg[2] },
|
|
179
|
+
memory: {
|
|
180
|
+
total: totalMem,
|
|
181
|
+
free: freeMem,
|
|
182
|
+
used: totalMem - freeMem,
|
|
183
|
+
pctUsed: ((1 - freeMem / totalMem) * 100).toFixed(1) + '%',
|
|
184
|
+
},
|
|
185
|
+
disk,
|
|
186
|
+
tailscaleIP,
|
|
187
|
+
interfaces,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function handleProcesses(req, res) {
|
|
192
|
+
const processNames = ['aiva-app', 'auto-deploy', 'health-reporter', 'diagnostics-api'];
|
|
193
|
+
const results = [];
|
|
194
|
+
|
|
195
|
+
// Try PM2 first
|
|
196
|
+
const pm2Json = safeExec('pm2 jlist 2>/dev/null');
|
|
197
|
+
let pm2Processes = [];
|
|
198
|
+
if (pm2Json) {
|
|
199
|
+
try { pm2Processes = JSON.parse(pm2Json); } catch {}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const name of processNames) {
|
|
203
|
+
const pm2Proc = pm2Processes.find(p => p.name === name);
|
|
204
|
+
if (pm2Proc) {
|
|
205
|
+
results.push({
|
|
206
|
+
name,
|
|
207
|
+
manager: 'pm2',
|
|
208
|
+
status: pm2Proc.pm2_env?.status || 'unknown',
|
|
209
|
+
pid: pm2Proc.pid,
|
|
210
|
+
uptime: pm2Proc.pm2_env?.pm_uptime ? Date.now() - pm2Proc.pm2_env.pm_uptime : null,
|
|
211
|
+
restarts: pm2Proc.pm2_env?.restart_time || 0,
|
|
212
|
+
memory: pm2Proc.monit?.memory || null,
|
|
213
|
+
cpu: pm2Proc.monit?.cpu || null,
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
// Check if running as regular process
|
|
217
|
+
const pid = safeExec(`pgrep -f "${name}" 2>/dev/null`);
|
|
218
|
+
results.push({
|
|
219
|
+
name,
|
|
220
|
+
manager: pid ? 'system' : 'none',
|
|
221
|
+
status: pid ? 'online' : 'stopped',
|
|
222
|
+
pid: pid ? parseInt(pid.split('\n')[0]) : null,
|
|
223
|
+
uptime: null,
|
|
224
|
+
restarts: null,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
sendJSON(res, 200, { processes: results });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function handleConfig(req, res) {
|
|
233
|
+
const envFile = path.join(AIVA_DIR, '.env');
|
|
234
|
+
try {
|
|
235
|
+
const content = fs.readFileSync(envFile, 'utf8');
|
|
236
|
+
const masked = content.split('\n').map(line => {
|
|
237
|
+
if (!line.includes('=') || line.startsWith('#')) return line;
|
|
238
|
+
const eqIdx = line.indexOf('=');
|
|
239
|
+
const key = line.slice(0, eqIdx);
|
|
240
|
+
return `${key}=***`;
|
|
241
|
+
}).join('\n');
|
|
242
|
+
sendJSON(res, 200, { config: masked });
|
|
243
|
+
} catch {
|
|
244
|
+
sendJSON(res, 200, { config: 'No .env file found' });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function handleDeployLog(req, res) {
|
|
249
|
+
const logFile = path.join(AIVA_DIR, 'data', 'auto-deploy.log');
|
|
250
|
+
const content = readLastLines(logFile, 50);
|
|
251
|
+
sendJSON(res, 200, { log: content || 'No deploy log found' });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- Server ---
|
|
255
|
+
|
|
256
|
+
const server = http.createServer((req, res) => {
|
|
257
|
+
// CORS preflight
|
|
258
|
+
if (req.method === 'OPTIONS') {
|
|
259
|
+
res.writeHead(204, {
|
|
260
|
+
'Access-Control-Allow-Origin': '*',
|
|
261
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
262
|
+
'Access-Control-Allow-Headers': 'Authorization, Content-Type',
|
|
263
|
+
});
|
|
264
|
+
return res.end();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (req.method !== 'GET') {
|
|
268
|
+
return sendJSON(res, 405, { error: 'Method not allowed' });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const parsed = url.parse(req.url, true);
|
|
272
|
+
const pathname = parsed.pathname;
|
|
273
|
+
|
|
274
|
+
// /health is unauthenticated
|
|
275
|
+
if (pathname === '/health') return handleHealth(req, res);
|
|
276
|
+
|
|
277
|
+
// All other routes require auth
|
|
278
|
+
if (!authenticate(req)) {
|
|
279
|
+
return sendJSON(res, 401, { error: 'Unauthorized — provide valid Bearer token' });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
switch (pathname) {
|
|
284
|
+
case '/logs': return handleLogs(req, res, parsed.query);
|
|
285
|
+
case '/errors': return handleErrors(req, res);
|
|
286
|
+
case '/system': return handleSystem(req, res);
|
|
287
|
+
case '/processes': return handleProcesses(req, res);
|
|
288
|
+
case '/config': return handleConfig(req, res);
|
|
289
|
+
case '/deploy-log': return handleDeployLog(req, res);
|
|
290
|
+
default: return sendJSON(res, 404, { error: 'Not found' });
|
|
291
|
+
}
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error('Handler error:', err);
|
|
294
|
+
sendJSON(res, 500, { error: 'Internal server error', detail: err.message });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
process.on('uncaughtException', (err) => {
|
|
299
|
+
console.error('Uncaught exception:', err);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
server.listen(PORT, '0.0.0.0', () => {
|
|
303
|
+
console.log(`Diagnostics API running on port ${PORT}`);
|
|
304
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Ara Voice Fix: Duplicate Response Audio Suppression
|
|
2
|
+
|
|
3
|
+
## File: `voice-call.js`
|
|
4
|
+
|
|
5
|
+
## Root Cause
|
|
6
|
+
xAI's Realtime API sometimes fires TWO complete response cycles for a single user turn. The current dedup only catches it at `response.done` — but by then the audio has already been streamed to the client via `response.output_audio.delta`. The user hears the same answer twice.
|
|
7
|
+
|
|
8
|
+
## Fix: Track response IDs and suppress duplicate audio
|
|
9
|
+
|
|
10
|
+
### Step 1: Add response tracking per call session
|
|
11
|
+
|
|
12
|
+
In the `callSession` object (wherever it's initialized, look for `callSession = {`), add:
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
completedResponseTexts: [], // track completed response texts
|
|
16
|
+
currentResponseId: null, // track current response ID
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Step 2: Capture response ID on `response.created`
|
|
20
|
+
|
|
21
|
+
In the `response.created` case, capture the response ID:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
case 'response.created':
|
|
25
|
+
callSession.currentResponseText = '';
|
|
26
|
+
callSession.currentResponseId = msg.response?.id || null;
|
|
27
|
+
socket.emit('voice-call-status', { status: 'speaking' });
|
|
28
|
+
break;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Step 3: Suppress audio at the delta level
|
|
32
|
+
|
|
33
|
+
Replace the `response.output_audio.delta` handler:
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
case 'response.output_audio.delta':
|
|
37
|
+
if (msg.delta) {
|
|
38
|
+
// Check if we're in a duplicate response by comparing accumulated text so far
|
|
39
|
+
// We'll use a flag set during transcript accumulation
|
|
40
|
+
if (!callSession.suppressCurrentResponse) {
|
|
41
|
+
socket.emit('voice-call-audio-delta', { audio: msg.delta });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Step 4: Detect duplicates during transcript accumulation
|
|
48
|
+
|
|
49
|
+
Replace the `response.output_audio_transcript.delta` handler:
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
case 'response.output_audio_transcript.delta':
|
|
53
|
+
if (msg.delta) {
|
|
54
|
+
callSession.currentResponseText += msg.delta;
|
|
55
|
+
// Once we have 50+ chars, check if this matches a previous response
|
|
56
|
+
if (!callSession.suppressCurrentResponse && callSession.currentResponseText.length >= 50) {
|
|
57
|
+
const prefix = callSession.currentResponseText.slice(0, 50);
|
|
58
|
+
if (callSession.completedResponseTexts.some(t => t.slice(0, 50) === prefix)) {
|
|
59
|
+
callSession.suppressCurrentResponse = true;
|
|
60
|
+
console.log('[voice-call] Suppressing duplicate response audio');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!callSession.suppressCurrentResponse) {
|
|
64
|
+
socket.emit('voice-call-transcript-delta', { text: msg.delta });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Step 5: Record completed responses and reset
|
|
71
|
+
|
|
72
|
+
Replace the `response.output_audio_transcript.done` handler:
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
case 'response.output_audio_transcript.done':
|
|
76
|
+
if (msg.transcript && !callSession.suppressCurrentResponse) {
|
|
77
|
+
callSession.transcript.push({ role: 'assistant', text: msg.transcript });
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Update the `response.done` handler — remove the old dedup logic and replace with:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
case 'response.done': {
|
|
86
|
+
socket.emit('voice-call-status', { status: 'listening' });
|
|
87
|
+
const respText = callSession.currentResponseText;
|
|
88
|
+
if (respText && !callSession.suppressCurrentResponse) {
|
|
89
|
+
callSession.completedResponseTexts.push(respText);
|
|
90
|
+
// Keep only last 5 to prevent memory growth
|
|
91
|
+
if (callSession.completedResponseTexts.length > 5) {
|
|
92
|
+
callSession.completedResponseTexts.shift();
|
|
93
|
+
}
|
|
94
|
+
socket.emit('voice-call-response-done', { text: respText });
|
|
95
|
+
}
|
|
96
|
+
// Reset for next response
|
|
97
|
+
callSession.suppressCurrentResponse = false;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Step 6: Clean up old dedup code
|
|
103
|
+
|
|
104
|
+
Remove the `lastResponseText` Map (line ~24) and its cleanup on disconnect (line ~711) since we're using per-session tracking now.
|
|
105
|
+
|
|
106
|
+
## After Changes
|
|
107
|
+
- `pm2 restart aiva-app`
|
|
108
|
+
- Verify started
|
|
109
|
+
|
|
110
|
+
## IMPORTANT
|
|
111
|
+
- Only modify `voice-call.js`
|
|
112
|
+
- Do NOT touch `public/index.html`
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Ara Voice Fix Round 2: Calendar Tool + Duplicate Responses
|
|
2
|
+
|
|
3
|
+
## File: `voice-call.js`
|
|
4
|
+
|
|
5
|
+
## Fix 1: Filter past events in `check_calendar` tool (around line 345-353)
|
|
6
|
+
|
|
7
|
+
The `check_calendar` tool handler returns ALL events including past ones. Add past-event filtering when `timeframe === 'today'`.
|
|
8
|
+
|
|
9
|
+
**REPLACE** the calResults return block (around line 352-353):
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
// Filter past events for today view
|
|
13
|
+
if (tf === 'today') {
|
|
14
|
+
calResults = calResults.filter(line => {
|
|
15
|
+
const timeMatch = line.match(/(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2})/);
|
|
16
|
+
if (timeMatch) {
|
|
17
|
+
const eventTime = new Date(timeMatch[1].replace(' ', 'T'));
|
|
18
|
+
return eventTime > now;
|
|
19
|
+
}
|
|
20
|
+
return true; // keep lines without parseable time
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return { success: true, calendar: calResults.join('\n') || 'No upcoming events today.' };
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Fix 2: Filter past events in `weekCalendar` pre-fetch (around line 87-102)
|
|
27
|
+
|
|
28
|
+
The week calendar pre-fetch does NOT filter past events. Add the same filter:
|
|
29
|
+
|
|
30
|
+
After `weekLines.push(...)` and before `result.weekCalendar = weekLines.join('\n')`, add:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// Filter out past events from this week too
|
|
34
|
+
weekLines = weekLines.filter(line => {
|
|
35
|
+
const timeMatch = line.match(/(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2})/);
|
|
36
|
+
if (timeMatch) {
|
|
37
|
+
const eventTime = new Date(timeMatch[1].replace(' ', 'T'));
|
|
38
|
+
return eventTime > now;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Fix 3: Prevent duplicate responses
|
|
45
|
+
|
|
46
|
+
Look for where `response.audio.done` or `response.done` events trigger sending audio to the client. If there's a risk of duplicate tool-call responses being sent, add a dedup guard. Specifically check if there's a pattern where both a tool result AND a follow-up response trigger separate audio sends for the same content.
|
|
47
|
+
|
|
48
|
+
If you see a `response.audio.done` handler that doesn't dedup, add a simple guard:
|
|
49
|
+
```javascript
|
|
50
|
+
// Track last response to prevent duplicates
|
|
51
|
+
const lastResponseText = new Map(); // callId -> lastText
|
|
52
|
+
```
|
|
53
|
+
Before sending audio response text, check if it's substantially the same as the last one sent (first 50 chars match). If so, skip it.
|
|
54
|
+
|
|
55
|
+
## After Changes
|
|
56
|
+
- `pm2 restart aiva-app`
|
|
57
|
+
- Verify started successfully
|
|
58
|
+
|
|
59
|
+
## IMPORTANT
|
|
60
|
+
- Only modify `voice-call.js`
|
|
61
|
+
- Do NOT touch `public/index.html` or microphone feature
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Ara Voice Call Fix: Greeting Behavior + Calendar Filtering
|
|
2
|
+
|
|
3
|
+
## File: `voice-call.js`
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
1. Ara dumps calendar events immediately on connect before understanding user intent
|
|
7
|
+
2. Calendar context includes past events (e.g., 7:30 AM and 11 AM events mentioned at 2:30 PM)
|
|
8
|
+
|
|
9
|
+
## Fix 1: System Prompt — Don't Volunteer Info
|
|
10
|
+
|
|
11
|
+
Change the greeting instruction (around line 147):
|
|
12
|
+
|
|
13
|
+
**FROM:**
|
|
14
|
+
```
|
|
15
|
+
- Start the conversation with a casual greeting like "Hey! What's up?"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**TO:**
|
|
19
|
+
```
|
|
20
|
+
- Start the conversation with a brief, casual greeting like "Hey Brandon, what's up?" and WAIT for the user to speak. Do NOT volunteer any information (calendar, tasks, etc.) until the user tells you what they need. Your job is to listen first, then assist. Only bring up calendar/tasks/context when the user asks or when it's directly relevant to what they're discussing.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Fix 2: Filter Past Calendar Events
|
|
24
|
+
|
|
25
|
+
In `buildSystemPrompt()` where calendar events are appended (around line 163-165), filter out events that have already passed:
|
|
26
|
+
|
|
27
|
+
**FROM:**
|
|
28
|
+
```javascript
|
|
29
|
+
if (ctx.calendar?.length) {
|
|
30
|
+
const events = ctx.calendar.slice(0, 5).map(e => `- ${e.title || e.summary} (${e.time || e.start || ''})`).join('\n');
|
|
31
|
+
prompt += `\n\nToday's calendar:\n${events}`;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**TO:**
|
|
36
|
+
```javascript
|
|
37
|
+
if (ctx.calendar?.length) {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const futureEvents = ctx.calendar.filter(e => {
|
|
40
|
+
const eventTime = e.time || e.start || '';
|
|
41
|
+
if (!eventTime) return true; // keep events with no time (all-day)
|
|
42
|
+
try {
|
|
43
|
+
const eventDate = new Date(eventTime);
|
|
44
|
+
return eventDate > now;
|
|
45
|
+
} catch { return true; }
|
|
46
|
+
});
|
|
47
|
+
if (futureEvents.length) {
|
|
48
|
+
const events = futureEvents.slice(0, 5).map(e => `- ${e.title || e.summary} (${e.time || e.start || ''})`).join('\n');
|
|
49
|
+
prompt += `\n\nUpcoming calendar events (DO NOT mention these unless asked):\n${events}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Also apply the same past-event filtering to the pre-fetch calendar sections (lines ~55-104) where `todayCalendar` and `weekCalendar` are built.
|
|
55
|
+
|
|
56
|
+
## Fix 3: Label Context as Reference-Only
|
|
57
|
+
|
|
58
|
+
Change the calendar/tasks/chat context labels to make it clear they're reference material, not conversation starters:
|
|
59
|
+
|
|
60
|
+
- `Today's calendar:` → `Upcoming calendar events (reference only — do NOT mention unless asked):`
|
|
61
|
+
- `Active tasks:` → `Active tasks (reference only — do NOT mention unless asked):`
|
|
62
|
+
- `Recent chat:` → `Recent chat context (reference only):`
|
|
63
|
+
|
|
64
|
+
## After Changes
|
|
65
|
+
- Restart: `pm2 restart aiva-app`
|
|
66
|
+
- Test by calling and confirming Ara greets briefly and waits
|
|
67
|
+
|
|
68
|
+
## IMPORTANT
|
|
69
|
+
- Do NOT touch the microphone icon (voice-to-text) — only the phone icon voice call feature
|
|
70
|
+
- Do NOT touch `public/index.html` — changes are server-side only in `voice-call.js`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Calendar Date Parsing Fix — Scope Doc
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
The `check_calendar` tool in `voice-call.js` uses relative date strings like `+7d` and `+2d` with the `gog calendar list` command, but `gog` doesn't support that syntax. It needs absolute ISO dates (e.g., `2026-02-11`) or keywords like `today`, `tomorrow`, `monday`.
|
|
5
|
+
|
|
6
|
+
## File
|
|
7
|
+
`/Users/brandonburgan/.openclaw/workspace/aiva-tasks/voice-call.js`
|
|
8
|
+
|
|
9
|
+
## Fix
|
|
10
|
+
In the `check_calendar` tool handler, replace any relative date logic (`+Xd`) with computed absolute dates using JavaScript's `Date` object.
|
|
11
|
+
|
|
12
|
+
Example: If `timeframe === 'week'`, compute:
|
|
13
|
+
```js
|
|
14
|
+
const from = new Date().toISOString().split('T')[0]; // today
|
|
15
|
+
const to = new Date(Date.now() + 7 * 86400000).toISOString().split('T')[0]; // 7 days out
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then pass `--from ${from} --to ${to}` to the `gog calendar list` command.
|
|
19
|
+
|
|
20
|
+
## Specific Changes
|
|
21
|
+
1. Find the `check_calendar` tool handler function
|
|
22
|
+
2. Replace any `+Xd` style date strings with computed absolute dates
|
|
23
|
+
3. Handle all timeframe values: `today`, `tomorrow`, `week`, `month`
|
|
24
|
+
4. Make sure the `--from` flag uses today's date (not a relative string)
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
After changes, restart PM2: `pm2 restart aiva-app`
|
|
28
|
+
Test by making a voice call and asking "What's on my calendar this week?"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Getting to Know Your AIVA Assistant
|
|
2
|
+
|
|
3
|
+
Welcome! AIVA (AI Virtual Assistant) is your personal AI-powered assistant designed to help you stay organized, communicate effectively, and get things done. Here's everything you need to know to get the most out of AIVA.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What is AIVA?
|
|
8
|
+
|
|
9
|
+
AIVA is more than a chatbot — she's a team of specialized AI agents working together behind the scenes. Think of it like having a personal office staff: each agent has a specific role, and they coordinate to handle your requests efficiently.
|
|
10
|
+
|
|
11
|
+
You don't need to worry about which agent does what — just communicate naturally, and AIVA routes everything to the right place.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## How AIVA is Set Up
|
|
16
|
+
|
|
17
|
+
AIVA uses a **multi-agent system**, meaning different agents handle different types of work:
|
|
18
|
+
|
|
19
|
+
### Main Agent (Your Central Brain)
|
|
20
|
+
This is the agent you talk to in the **AIVA App**. It's the coordinator — the one that:
|
|
21
|
+
- Receives your requests and figures out who should handle them
|
|
22
|
+
- Manages your task board
|
|
23
|
+
- Distributes knowledge across all other agents
|
|
24
|
+
- Handles calendar, email monitoring, and general questions
|
|
25
|
+
- Sends you morning and evening briefings
|
|
26
|
+
|
|
27
|
+
**Think of it as your executive assistant** who delegates to specialists.
|
|
28
|
+
|
|
29
|
+
### Outreach Agent (Your Message Manager)
|
|
30
|
+
This agent handles all **iMessage and text communication**. When someone texts your AIVA-managed number:
|
|
31
|
+
- The outreach agent reads and responds (based on your preferences)
|
|
32
|
+
- It manages conversations with contacts on your behalf
|
|
33
|
+
- It follows up on active conversations automatically
|
|
34
|
+
- It surfaces important messages to the main agent when needed
|
|
35
|
+
|
|
36
|
+
**Think of it as your receptionist** — fielding calls and messages so you don't have to.
|
|
37
|
+
|
|
38
|
+
### Email Agent (Your Inbox Manager)
|
|
39
|
+
Monitors your email, triages messages, and alerts you to anything important. Routine stuff gets handled automatically so you only see what matters.
|
|
40
|
+
|
|
41
|
+
### Research Agent (Your Analyst)
|
|
42
|
+
When you need information — market research, competitor analysis, or deep dives on a topic — the research agent handles it and reports back with findings.
|
|
43
|
+
|
|
44
|
+
### Content Agent (Your Writer)
|
|
45
|
+
Handles content creation tasks — drafts, copy, documentation, social posts, and more.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## How to Communicate with AIVA
|
|
50
|
+
|
|
51
|
+
### The AIVA App (Your Main Channel)
|
|
52
|
+
The AIVA app is your **central hub**. This is where you should:
|
|
53
|
+
- **Brain dump** — tell AIVA everything on your mind. She'll organize it, create tasks, and distribute knowledge to the right agents.
|
|
54
|
+
- **Make requests** — "Schedule a meeting with John next week" or "Research the best CRM tools for small businesses"
|
|
55
|
+
- **Check your task board** — see what's in progress, what's done, and what needs attention
|
|
56
|
+
- **Review agent activity** — see what your agents are working on
|
|
57
|
+
|
|
58
|
+
**Pro tip:** Voice messages work great in the app. Just talk naturally — AIVA will transcribe and act on it.
|
|
59
|
+
|
|
60
|
+
### iMessage (Your Outreach Channel)
|
|
61
|
+
Messages sent to your AIVA phone number go directly to the **outreach agent**. This is specifically for:
|
|
62
|
+
- Managing conversations with contacts
|
|
63
|
+
- Scheduling and follow-ups
|
|
64
|
+
- Having your outreach agent handle communication on your behalf
|
|
65
|
+
|
|
66
|
+
**Important:** The outreach agent is a separate specialist from your main AIVA. Training it happens through iMessage conversations. The more context you give it about how you want to handle specific contacts, the better it gets.
|
|
67
|
+
|
|
68
|
+
### What Happens When You Submit a Request
|
|
69
|
+
|
|
70
|
+
1. **You send a message** (via app, voice, or text)
|
|
71
|
+
2. **AIVA receives it** and creates a task on your board
|
|
72
|
+
3. **The right agent picks it up** based on what needs to be done
|
|
73
|
+
4. **Work happens** — you can check progress on the task board anytime
|
|
74
|
+
5. **You get notified** when it's done, with a summary of what was accomplished
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Best Practices
|
|
79
|
+
|
|
80
|
+
### Do This
|
|
81
|
+
- **Be specific** — "Schedule a 30-min call with John on Thursday at 2 PM" works better than "set something up with John"
|
|
82
|
+
- **Use the app for big-picture stuff** — strategy, planning, brain dumps, complex requests
|
|
83
|
+
- **Use iMessage for message management** — training your outreach agent on how to handle contacts
|
|
84
|
+
- **Check your task board** — it's the single source of truth for all work in progress
|
|
85
|
+
- **Give feedback** — if AIVA does something you don't like, say so. She learns from your preferences.
|
|
86
|
+
|
|
87
|
+
### Avoid This
|
|
88
|
+
- **Don't repeat yourself** — if you submitted a request, it's on the board. No need to follow up unless it's been too long.
|
|
89
|
+
- **Don't mix channels unnecessarily** — use the app for requests, iMessage for message management
|
|
90
|
+
- **Don't assume AIVA knows everything** — she wakes up fresh each session. Important context should be stated clearly.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Frequently Asked Questions
|
|
95
|
+
|
|
96
|
+
**Q: Does AIVA remember our conversations?**
|
|
97
|
+
A: AIVA maintains memory through daily notes and long-term memory files. Important decisions, preferences, and context are saved between sessions. However, she starts each session fresh and reads her notes — so the more important something is, the more clearly you should state it.
|
|
98
|
+
|
|
99
|
+
**Q: Can AIVA text people on my behalf?**
|
|
100
|
+
A: Yes! The outreach agent can handle iMessage conversations. You set preferences for each contact (respond, ignore, take message only), and AIVA follows them. She'll always check with you before contacting someone new.
|
|
101
|
+
|
|
102
|
+
**Q: How do I know what AIVA is working on?**
|
|
103
|
+
A: Check the task board in the app. Every request becomes a task with status tracking (To Do, In Progress, Done).
|
|
104
|
+
|
|
105
|
+
**Q: What if AIVA makes a mistake?**
|
|
106
|
+
A: Tell her! Feedback helps AIVA improve. She logs lessons learned and adjusts her approach going forward.
|
|
107
|
+
|
|
108
|
+
**Q: Is my information private?**
|
|
109
|
+
A: Yes. AIVA treats your data with strict privacy. She won't share personal information in group settings or with other contacts without your explicit permission.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Updates
|
|
114
|
+
|
|
115
|
+
*Check the Updates page for the latest changes and improvements to your AIVA assistant.*
|