@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.
- package/bin/aiva.js +26 -14
- package/lib/bluebubbles.js +145 -0
- package/lib/config-gen.js +253 -0
- package/lib/constants.js +72 -0
- package/lib/launch-agent.js +112 -0
- package/lib/prerequisites.js +236 -0
- package/lib/process.js +59 -145
- package/lib/setup.js +224 -194
- package/lib/validate.js +194 -0
- package/package.json +9 -34
- package/auto-deploy.js +0 -190
- package/cli-sync.js +0 -126
- package/d2a-prompt-template.txt +0 -106
- package/diagnostics-api.js +0 -304
- package/docs/ara-dedup-fix-scope.md +0 -112
- package/docs/ara-fix-round2-scope.md +0 -61
- package/docs/ara-greeting-fix-scope.md +0 -70
- package/docs/calendar-date-fix-scope.md +0 -28
- package/docs/getting-started.md +0 -115
- package/docs/network-architecture-rollout-scope.md +0 -43
- package/docs/scope-google-oauth-integration.md +0 -351
- package/docs/settings-page-scope.md +0 -50
- package/docs/xai-imagine-scope.md +0 -116
- package/docs/xai-voice-integration-scope.md +0 -115
- package/docs/xai-voice-tools-scope.md +0 -165
- package/email-router.js +0 -512
- package/follow-up-handler.js +0 -606
- package/gateway-monitor.js +0 -158
- package/google-email.js +0 -379
- package/google-oauth.js +0 -310
- package/grok-imagine.js +0 -97
- package/health-reporter.js +0 -287
- package/invisible-prefix-base.txt +0 -206
- package/invisible-prefix-owner.txt +0 -26
- package/invisible-prefix-slim.txt +0 -10
- package/invisible-prefix.txt +0 -43
- package/knowledge-base.js +0 -472
- package/lib/cli.js +0 -19
- package/lib/server.js +0 -42
- package/meta-capi.js +0 -206
- package/meta-leads.js +0 -411
- package/notion-oauth.js +0 -323
- package/public/agent-config.html +0 -241
- package/public/aiva-avatar-anime.png +0 -0
- package/public/css/docs.css.bak +0 -688
- package/public/css/onboarding.css +0 -543
- package/public/diagrams/claude-subscription-pool.html +0 -329
- package/public/diagrams/claude-subscription-pool.png +0 -0
- package/public/docs-icon.png +0 -0
- package/public/escalation.html +0 -237
- package/public/group-config.html +0 -300
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icons/agents.svg +0 -1
- package/public/icons/attach.svg +0 -1
- package/public/icons/characters.svg +0 -1
- package/public/icons/chat.svg +0 -1
- package/public/icons/docs.svg +0 -1
- package/public/icons/heartbeat.svg +0 -1
- package/public/icons/messages.svg +0 -1
- package/public/icons/mic.svg +0 -1
- package/public/icons/notes.svg +0 -1
- package/public/icons/settings.svg +0 -1
- package/public/icons/tasks.svg +0 -1
- 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 +0 -11594
- package/public/js/onboarding.js +0 -699
- package/public/manifest.json +0 -24
- package/public/messages-v2.html +0 -2824
- package/public/permission-approve.html.bak +0 -107
- package/public/permissions.html +0 -150
- package/public/styles/design-system.css +0 -68
- package/router-db.js +0 -604
- package/router-utils.js +0 -28
- package/router-v2/adapters/imessage.js +0 -191
- package/router-v2/adapters/quo.js +0 -82
- package/router-v2/adapters/whatsapp.js +0 -192
- package/router-v2/contact-manager.js +0 -234
- package/router-v2/conversation-engine.js +0 -498
- package/router-v2/data/knowledge-base.json +0 -176
- 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 +0 -457
- package/router-v2/escalation-bridge.js +0 -540
- package/router-v2/follow-up-engine.js +0 -347
- package/router-v2/index.js +0 -441
- package/router-v2/ingestion.js +0 -213
- package/router-v2/knowledge-base.js +0 -231
- package/router-v2/lead-qualifier.js +0 -152
- package/router-v2/learning-loop.js +0 -202
- package/router-v2/outbound-sender.js +0 -160
- package/router-v2/package.json +0 -13
- package/router-v2/permission-gate.js +0 -86
- package/router-v2/playbook.js +0 -177
- package/router-v2/prompts/base.js +0 -52
- package/router-v2/prompts/first-contact.js +0 -38
- package/router-v2/prompts/lead-qualification.js +0 -37
- package/router-v2/prompts/scheduling.js +0 -72
- package/router-v2/prompts/style-overrides.js +0 -22
- package/router-v2/scheduler.js +0 -301
- package/router-v2/scripts/migrate-v1-to-v2.js +0 -215
- package/router-v2/scripts/seed-faq.js +0 -67
- package/router-v2/seed-knowledge-base.js +0 -39
- package/router-v2/utils/ai.js +0 -129
- package/router-v2/utils/phone.js +0 -52
- package/router-v2/utils/response-validator.js +0 -98
- package/router-v2/utils/sanitize.js +0 -222
- package/router.js +0 -5005
- package/routes/google-calendar.js +0 -186
- package/scripts/deploy.sh +0 -62
- package/scripts/macos-calendar.sh +0 -232
- package/scripts/onboard-device.sh +0 -466
- package/server.js +0 -5131
- package/start.sh +0 -24
- package/templates/AGENTS.md +0 -548
- package/templates/IDENTITY.md +0 -15
- package/templates/docs-agents.html +0 -132
- package/templates/docs-app.html +0 -130
- package/templates/docs-home.html +0 -83
- package/templates/docs-imessage.html +0 -121
- package/templates/docs-tasks.html +0 -123
- package/templates/docs-tips.html +0 -175
- package/templates/getting-started.html +0 -809
- package/templates/invisible-prefix-base.txt +0 -171
- package/templates/invisible-prefix-owner.txt +0 -282
- package/templates/invisible-prefix.txt +0 -338
- package/templates/manifest.json +0 -61
- package/templates/memory-org/clients.md +0 -7
- package/templates/memory-org/credentials.md +0 -9
- package/templates/memory-org/devices.md +0 -7
- package/templates/updates.html +0 -464
- package/tts-proxy.js +0 -96
- package/voice-call-local.js +0 -731
- package/voice-call.js +0 -732
- package/wa-listener.js +0 -354
package/bin/aiva.js
CHANGED
|
@@ -3,19 +3,18 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
const { Command } = require('commander');
|
|
6
|
-
const path = require('path');
|
|
7
6
|
const pkg = require('../package.json');
|
|
8
7
|
|
|
9
8
|
const program = new Command();
|
|
10
9
|
|
|
11
10
|
program
|
|
12
11
|
.name('aiva')
|
|
13
|
-
.description('AIVA -
|
|
12
|
+
.description('AIVA CLI - Provisions a fresh Mac as an AIVA client device')
|
|
14
13
|
.version(pkg.version, '-v, --version');
|
|
15
14
|
|
|
16
15
|
program
|
|
17
16
|
.command('setup')
|
|
18
|
-
.description('
|
|
17
|
+
.description('Full interactive setup wizard - provisions this Mac as an AIVA device')
|
|
19
18
|
.option('--non-interactive', 'Skip interactive prompts, use defaults')
|
|
20
19
|
.option('--port <port>', 'Server port', '3847')
|
|
21
20
|
.option('--name <name>', 'Assistant name', 'AIVA')
|
|
@@ -23,12 +22,20 @@ program
|
|
|
23
22
|
require('../lib/setup').run(opts);
|
|
24
23
|
});
|
|
25
24
|
|
|
25
|
+
program
|
|
26
|
+
.command('validate')
|
|
27
|
+
.description('Run the validation checklist (SOP Step 11)')
|
|
28
|
+
.action(async () => {
|
|
29
|
+
const { runValidation } = require('../lib/validate');
|
|
30
|
+
const { passed, failed } = await runValidation();
|
|
31
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
26
34
|
program
|
|
27
35
|
.command('start')
|
|
28
|
-
.description('Start the AIVA server')
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
require('../lib/process').startServer(opts);
|
|
36
|
+
.description('Start the AIVA server via LaunchAgent')
|
|
37
|
+
.action(() => {
|
|
38
|
+
require('../lib/process').startServer();
|
|
32
39
|
});
|
|
33
40
|
|
|
34
41
|
program
|
|
@@ -60,13 +67,6 @@ program
|
|
|
60
67
|
require('../lib/process').getLogs(parseInt(opts.lines, 10));
|
|
61
68
|
});
|
|
62
69
|
|
|
63
|
-
program
|
|
64
|
-
.command('update')
|
|
65
|
-
.description('Update AIVA to the latest version')
|
|
66
|
-
.action(() => {
|
|
67
|
-
require('../lib/cli').update();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
70
|
program
|
|
71
71
|
.command('config')
|
|
72
72
|
.description('Show current configuration')
|
|
@@ -74,6 +74,18 @@ program
|
|
|
74
74
|
require('../lib/config').printConfig();
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
program
|
|
78
|
+
.command('encrypt-ts-key')
|
|
79
|
+
.description('Utility: encrypt a Tailscale auth key with a password')
|
|
80
|
+
.argument('<tskey>', 'Tailscale auth key')
|
|
81
|
+
.argument('<password>', 'Encryption password (the device activation code)')
|
|
82
|
+
.action((tskey, password) => {
|
|
83
|
+
const { encryptTailscaleKey } = require('../lib/constants');
|
|
84
|
+
const result = encryptTailscaleKey(tskey, password);
|
|
85
|
+
console.log(JSON.stringify(result, null, 2));
|
|
86
|
+
console.log('\nPaste this into lib/constants.js TAILSCALE_ENCRYPTED_KEY');
|
|
87
|
+
});
|
|
88
|
+
|
|
77
89
|
program.parse(process.argv);
|
|
78
90
|
|
|
79
91
|
if (!process.argv.slice(2).length) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const prompts = require('prompts');
|
|
4
|
+
const pc = require('picocolors');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const { section, ok, warn, info } = require('./prerequisites');
|
|
7
|
+
const { BLUEBUBBLES_PORT } = require('./constants');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Interactive BlueBubbles/iMessage setup walkthrough
|
|
11
|
+
*/
|
|
12
|
+
async function setupBlueBubbles() {
|
|
13
|
+
section('BlueBubbles / iMessage Setup');
|
|
14
|
+
console.log();
|
|
15
|
+
info('BlueBubbles enables iMessage integration for AIVA.');
|
|
16
|
+
console.log();
|
|
17
|
+
|
|
18
|
+
// Step 1: Check if installed
|
|
19
|
+
const { installed } = await prompts({
|
|
20
|
+
type: 'confirm',
|
|
21
|
+
name: 'installed',
|
|
22
|
+
message: 'Do you have BlueBubbles installed?',
|
|
23
|
+
initial: true
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!installed) {
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(pc.cyan(' Download BlueBubbles from: https://bluebubbles.app'));
|
|
29
|
+
console.log(pc.cyan(' Install it, then come back here.'));
|
|
30
|
+
console.log();
|
|
31
|
+
|
|
32
|
+
const { ready } = await prompts({
|
|
33
|
+
type: 'confirm',
|
|
34
|
+
name: 'ready',
|
|
35
|
+
message: 'BlueBubbles installed and ready?',
|
|
36
|
+
initial: true
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!ready) {
|
|
40
|
+
warn('Skipping BlueBubbles setup. You can configure it later.');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Step 2: Sign in reminder
|
|
46
|
+
console.log();
|
|
47
|
+
info('Make sure BlueBubbles is open and signed in with the Apple ID for this device.');
|
|
48
|
+
|
|
49
|
+
const { signedIn } = await prompts({
|
|
50
|
+
type: 'confirm',
|
|
51
|
+
name: 'signedIn',
|
|
52
|
+
message: 'BlueBubbles is open and signed in?',
|
|
53
|
+
initial: true
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!signedIn) {
|
|
57
|
+
warn('Please sign in to BlueBubbles first, then re-run setup.');
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 3: Get server password
|
|
62
|
+
const { bbPassword } = await prompts({
|
|
63
|
+
type: 'password',
|
|
64
|
+
name: 'bbPassword',
|
|
65
|
+
message: 'Enter your BlueBubbles server password:'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!bbPassword) {
|
|
69
|
+
warn('No password provided. Skipping webhook registration.');
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Step 4: Register webhook
|
|
74
|
+
console.log();
|
|
75
|
+
info('Registering AIVA webhook with BlueBubbles...');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const webhookUrl = `http://127.0.0.1:3847/webhook/bluebubbles`;
|
|
79
|
+
const result = await registerWebhook(bbPassword, webhookUrl);
|
|
80
|
+
if (result) {
|
|
81
|
+
ok('Webhook registered successfully');
|
|
82
|
+
} else {
|
|
83
|
+
warn('Webhook registration may have failed. Check BlueBubbles settings.');
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
warn(`Webhook registration failed: ${e.message}`);
|
|
87
|
+
info('You can register manually in BlueBubbles settings.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 5: Test message
|
|
91
|
+
console.log();
|
|
92
|
+
console.log(pc.cyan(' Send a test iMessage from your phone to this device to verify.'));
|
|
93
|
+
|
|
94
|
+
const { tested } = await prompts({
|
|
95
|
+
type: 'confirm',
|
|
96
|
+
name: 'tested',
|
|
97
|
+
message: 'Test message received successfully?',
|
|
98
|
+
initial: true
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (tested) {
|
|
102
|
+
ok('BlueBubbles setup complete');
|
|
103
|
+
} else {
|
|
104
|
+
warn('BlueBubbles may need additional configuration');
|
|
105
|
+
info('Check BlueBubbles app for connection status');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { password: bbPassword };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function registerWebhook(password, webhookUrl) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const data = JSON.stringify({
|
|
114
|
+
url: webhookUrl,
|
|
115
|
+
events: ["new-message", "updated-message"]
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const req = http.request({
|
|
119
|
+
hostname: '127.0.0.1',
|
|
120
|
+
port: BLUEBUBBLES_PORT,
|
|
121
|
+
path: `/api/v1/webhook?password=${encodeURIComponent(password)}`,
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
'Content-Length': data.length
|
|
126
|
+
}
|
|
127
|
+
}, (res) => {
|
|
128
|
+
let body = '';
|
|
129
|
+
res.on('data', chunk => body += chunk);
|
|
130
|
+
res.on('end', () => {
|
|
131
|
+
resolve(res.statusCode < 400);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
req.on('error', reject);
|
|
136
|
+
req.setTimeout(5000, () => {
|
|
137
|
+
req.destroy();
|
|
138
|
+
reject(new Error('Connection timeout'));
|
|
139
|
+
});
|
|
140
|
+
req.write(data);
|
|
141
|
+
req.end();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { setupBlueBubbles };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const pc = require('picocolors');
|
|
7
|
+
const { GATEWAY_URL, DEFAULT_HOOKS_TOKEN, generateToken } = require('./constants');
|
|
8
|
+
const { ok, warn, info, section } = require('./prerequisites');
|
|
9
|
+
|
|
10
|
+
function getAppDir() {
|
|
11
|
+
return path.join(os.homedir(), '.openclaw', 'workspace', 'aiva-tasks');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getOpenClawDir() {
|
|
15
|
+
return path.join(os.homedir(), '.openclaw');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate openclaw.json with ALL required fields from the SOP
|
|
20
|
+
*/
|
|
21
|
+
function generateOpenClawConfig(clientData) {
|
|
22
|
+
const hooksToken = clientData.hooksToken || generateToken();
|
|
23
|
+
const homeDir = os.homedir();
|
|
24
|
+
const user = os.userInfo().username;
|
|
25
|
+
|
|
26
|
+
const config = {
|
|
27
|
+
hooks: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
token: hooksToken
|
|
30
|
+
},
|
|
31
|
+
channels: {
|
|
32
|
+
aiva: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
blockStreaming: true,
|
|
35
|
+
blockStreamingBreak: "message_end",
|
|
36
|
+
authToken: "",
|
|
37
|
+
routing: {
|
|
38
|
+
mainSessionUsers: [clientData.userId || "owner"]
|
|
39
|
+
},
|
|
40
|
+
dm: {
|
|
41
|
+
policy: "open"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
gateway: {
|
|
46
|
+
http: {
|
|
47
|
+
endpoints: {
|
|
48
|
+
chatCompletions: {
|
|
49
|
+
enabled: true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
auth: {
|
|
54
|
+
mode: "password",
|
|
55
|
+
password: clientData.gatewayPassword || require('crypto').randomBytes(16).toString('hex')
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
agents: {
|
|
59
|
+
defaults: {
|
|
60
|
+
blockStreamingDefault: "on",
|
|
61
|
+
blockStreamingBreak: "text_end",
|
|
62
|
+
blockStreamingCoalesce: {
|
|
63
|
+
minChars: 300,
|
|
64
|
+
idleMs: 1500
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
plugins: {
|
|
69
|
+
entries: {
|
|
70
|
+
aiva: { enabled: true }
|
|
71
|
+
},
|
|
72
|
+
load: {
|
|
73
|
+
paths: [`${homeDir}/.openclaw/extensions/aiva`]
|
|
74
|
+
},
|
|
75
|
+
installs: {
|
|
76
|
+
aiva: {
|
|
77
|
+
source: "path",
|
|
78
|
+
installPath: `${homeDir}/.openclaw/extensions/aiva`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
tools: {
|
|
83
|
+
exec: {
|
|
84
|
+
notifyOnExit: false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return { config, hooksToken };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate auth-profiles.json in the CORRECT format (SOP Known Gotchas)
|
|
94
|
+
*/
|
|
95
|
+
function generateAuthProfiles(anthropicKey) {
|
|
96
|
+
return {
|
|
97
|
+
profiles: {
|
|
98
|
+
"anthropic:key1": {
|
|
99
|
+
type: "token",
|
|
100
|
+
provider: "anthropic",
|
|
101
|
+
token: anthropicKey
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate connection.json - MUST use localhost
|
|
109
|
+
*/
|
|
110
|
+
function generateConnectionJson(hooksToken) {
|
|
111
|
+
return {
|
|
112
|
+
gatewayUrl: GATEWAY_URL,
|
|
113
|
+
gatewayToken: hooksToken
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate users.json - owner + client user
|
|
119
|
+
*/
|
|
120
|
+
function generateUsersJson(clientData) {
|
|
121
|
+
return {
|
|
122
|
+
users: [
|
|
123
|
+
{ id: clientData.ownerId || "owner", name: clientData.ownerName || "Admin", role: "owner" },
|
|
124
|
+
{
|
|
125
|
+
id: clientData.userId,
|
|
126
|
+
name: clientData.clientName,
|
|
127
|
+
role: "user"
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generate .env file for the AIVA app
|
|
135
|
+
*/
|
|
136
|
+
function generateDotEnv(config) {
|
|
137
|
+
const lines = [
|
|
138
|
+
`PORT=${config.port || 3847}`,
|
|
139
|
+
`CODING_ENABLED=false`,
|
|
140
|
+
`AIVA_CHANNEL_PLUGIN=true`
|
|
141
|
+
];
|
|
142
|
+
if (config.openaiKey) {
|
|
143
|
+
lines.push(`OPENAI_API_KEY=${config.openaiKey}`);
|
|
144
|
+
}
|
|
145
|
+
if (config.elevenLabsKey) {
|
|
146
|
+
lines.push(`ELEVENLABS_API_KEY=${config.elevenLabsKey}`);
|
|
147
|
+
}
|
|
148
|
+
return lines.join('\n') + '\n';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Write ALL config files to their correct locations
|
|
153
|
+
*/
|
|
154
|
+
function writeAllConfigs(clientData, apiKeys) {
|
|
155
|
+
section('Configuration Files');
|
|
156
|
+
|
|
157
|
+
const appDir = getAppDir();
|
|
158
|
+
const openclawDir = getOpenClawDir();
|
|
159
|
+
const agentDir = path.join(openclawDir, 'agents', 'main', 'agent');
|
|
160
|
+
|
|
161
|
+
// Ensure directories
|
|
162
|
+
for (const dir of [appDir, agentDir, path.join(appDir, 'data')]) {
|
|
163
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 1. openclaw.json
|
|
167
|
+
const { config: oclConfig, hooksToken } = generateOpenClawConfig(clientData);
|
|
168
|
+
const openclawJsonPath = path.join(openclawDir, 'openclaw.json');
|
|
169
|
+
fs.writeFileSync(openclawJsonPath, JSON.stringify(oclConfig, null, 2) + '\n');
|
|
170
|
+
ok('openclaw.json');
|
|
171
|
+
|
|
172
|
+
// 2. auth-profiles.json (correct format!)
|
|
173
|
+
const authProfiles = generateAuthProfiles(apiKeys.anthropicKey);
|
|
174
|
+
const authProfilesPath = path.join(agentDir, 'auth-profiles.json');
|
|
175
|
+
fs.writeFileSync(authProfilesPath, JSON.stringify(authProfiles, null, 2) + '\n');
|
|
176
|
+
ok('auth-profiles.json (correct format)');
|
|
177
|
+
|
|
178
|
+
// 3. connection.json
|
|
179
|
+
const connectionJson = generateConnectionJson(hooksToken);
|
|
180
|
+
const connectionPath = path.join(appDir, 'data', 'connection.json');
|
|
181
|
+
fs.writeFileSync(connectionPath, JSON.stringify(connectionJson, null, 2) + '\n');
|
|
182
|
+
ok('connection.json (localhost)');
|
|
183
|
+
|
|
184
|
+
// 4. users.json (in app root, NOT just data/)
|
|
185
|
+
const usersJson = generateUsersJson(clientData);
|
|
186
|
+
const usersPath = path.join(appDir, 'users.json');
|
|
187
|
+
fs.writeFileSync(usersPath, JSON.stringify(usersJson, null, 2) + '\n');
|
|
188
|
+
ok('users.json (owner + client)');
|
|
189
|
+
|
|
190
|
+
// 5. .env
|
|
191
|
+
const dotEnv = generateDotEnv({
|
|
192
|
+
port: clientData.port || 3847,
|
|
193
|
+
openaiKey: apiKeys.openaiKey,
|
|
194
|
+
elevenLabsKey: apiKeys.elevenLabsKey
|
|
195
|
+
});
|
|
196
|
+
fs.writeFileSync(path.join(appDir, '.env'), dotEnv);
|
|
197
|
+
ok('.env');
|
|
198
|
+
|
|
199
|
+
return { hooksToken, appDir };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Set up channel plugin symlink
|
|
204
|
+
*/
|
|
205
|
+
function setupChannelPlugin() {
|
|
206
|
+
section('Channel Plugin');
|
|
207
|
+
|
|
208
|
+
const homeDir = os.homedir();
|
|
209
|
+
const appDir = getAppDir();
|
|
210
|
+
const pluginSource = path.join(appDir, 'plugins', 'aiva-channel');
|
|
211
|
+
const extensionsDir = path.join(homeDir, '.openclaw', 'extensions');
|
|
212
|
+
const symlinkTarget = path.join(extensionsDir, 'aiva');
|
|
213
|
+
|
|
214
|
+
if (!fs.existsSync(extensionsDir)) {
|
|
215
|
+
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Remove existing symlink if it exists
|
|
219
|
+
try {
|
|
220
|
+
if (fs.existsSync(symlinkTarget) || fs.lstatSync(symlinkTarget)) {
|
|
221
|
+
fs.unlinkSync(symlinkTarget);
|
|
222
|
+
}
|
|
223
|
+
} catch { /* doesn't exist, fine */ }
|
|
224
|
+
|
|
225
|
+
if (fs.existsSync(pluginSource)) {
|
|
226
|
+
fs.symlinkSync(pluginSource, symlinkTarget);
|
|
227
|
+
ok('Channel plugin symlinked');
|
|
228
|
+
|
|
229
|
+
// Verify the plugin manifest exists
|
|
230
|
+
const manifest = path.join(symlinkTarget, 'openclaw.plugin.json');
|
|
231
|
+
if (fs.existsSync(manifest)) {
|
|
232
|
+
ok('Plugin manifest found');
|
|
233
|
+
} else {
|
|
234
|
+
warn('Plugin manifest not found at expected location');
|
|
235
|
+
info('Check that plugins/aiva-channel/openclaw.plugin.json exists in the app');
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
warn(`Plugin source not found: ${pluginSource}`);
|
|
239
|
+
info('Channel plugin will need to be set up manually after app is cloned');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
generateOpenClawConfig,
|
|
245
|
+
generateAuthProfiles,
|
|
246
|
+
generateConnectionJson,
|
|
247
|
+
generateUsersJson,
|
|
248
|
+
generateDotEnv,
|
|
249
|
+
writeAllConfigs,
|
|
250
|
+
setupChannelPlugin,
|
|
251
|
+
getAppDir,
|
|
252
|
+
getOpenClawDir
|
|
253
|
+
};
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// GitHub deploy token (read-only, repo scope) for cloning the AIVA app
|
|
6
|
+
const GITHUB_DEPLOY_TOKEN = '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';
|
|
7
|
+
const GITHUB_CLONE_URL = `https://${GITHUB_DEPLOY_TOKEN}@github.com/mistermakeithappen/aiva-app.git`;
|
|
8
|
+
|
|
9
|
+
// Tailscale auth key - encrypted with AES-256-CBC
|
|
10
|
+
// The decryption password is given to each client as their "device activation code"
|
|
11
|
+
// TODO: Brandon to provide real encrypted key. This is a placeholder.
|
|
12
|
+
const TAILSCALE_ENCRYPTED_KEY = {
|
|
13
|
+
cipher: 'aes-256-cbc',
|
|
14
|
+
// Placeholder - replace with real encrypted Tailscale auth key
|
|
15
|
+
data: 'PLACEHOLDER_ENCRYPTED_TAILSCALE_KEY',
|
|
16
|
+
iv: 'PLACEHOLDER_IV'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Default hooks token pattern
|
|
20
|
+
const DEFAULT_HOOKS_TOKEN = 'aiva-hook-secret-2026';
|
|
21
|
+
|
|
22
|
+
// App port
|
|
23
|
+
const APP_PORT = 3847;
|
|
24
|
+
const GATEWAY_PORT = 18789;
|
|
25
|
+
const GATEWAY_URL = `http://127.0.0.1:${GATEWAY_PORT}`;
|
|
26
|
+
|
|
27
|
+
// BlueBubbles default port
|
|
28
|
+
const BLUEBUBBLES_PORT = 1234;
|
|
29
|
+
|
|
30
|
+
function generateToken() {
|
|
31
|
+
return crypto.randomBytes(24).toString('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decryptTailscaleKey(password) {
|
|
35
|
+
if (TAILSCALE_ENCRYPTED_KEY.data === 'PLACEHOLDER_ENCRYPTED_TAILSCALE_KEY') {
|
|
36
|
+
return null; // Placeholder - not yet configured
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const key = crypto.scryptSync(password, 'aiva-tailscale-salt', 32);
|
|
40
|
+
const iv = Buffer.from(TAILSCALE_ENCRYPTED_KEY.iv, 'hex');
|
|
41
|
+
const decipher = crypto.createDecipheriv(TAILSCALE_ENCRYPTED_KEY.cipher, key, iv);
|
|
42
|
+
let decrypted = decipher.update(TAILSCALE_ENCRYPTED_KEY.data, 'hex', 'utf8');
|
|
43
|
+
decrypted += decipher.final('utf8');
|
|
44
|
+
return decrypted;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Encrypt a Tailscale key with a password (utility for Brandon to generate the encrypted blob)
|
|
51
|
+
function encryptTailscaleKey(tsKey, password) {
|
|
52
|
+
const key = crypto.scryptSync(password, 'aiva-tailscale-salt', 32);
|
|
53
|
+
const iv = crypto.randomBytes(16);
|
|
54
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
55
|
+
let encrypted = cipher.update(tsKey, 'utf8', 'hex');
|
|
56
|
+
encrypted += cipher.final('hex');
|
|
57
|
+
return { cipher: 'aes-256-cbc', data: encrypted, iv: iv.toString('hex') };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
GITHUB_DEPLOY_TOKEN,
|
|
62
|
+
GITHUB_CLONE_URL,
|
|
63
|
+
TAILSCALE_ENCRYPTED_KEY,
|
|
64
|
+
DEFAULT_HOOKS_TOKEN,
|
|
65
|
+
APP_PORT,
|
|
66
|
+
GATEWAY_PORT,
|
|
67
|
+
GATEWAY_URL,
|
|
68
|
+
BLUEBUBBLES_PORT,
|
|
69
|
+
generateToken,
|
|
70
|
+
decryptTailscaleKey,
|
|
71
|
+
encryptTailscaleKey
|
|
72
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
const { ok, warn, info, section } = require('./prerequisites');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate and install the LaunchAgent plist for client devices.
|
|
11
|
+
* Uses com.aiva.tasks.plist pattern from SOP.
|
|
12
|
+
*/
|
|
13
|
+
function setupLaunchAgent(appDir) {
|
|
14
|
+
section('LaunchAgent (Auto-Start)');
|
|
15
|
+
|
|
16
|
+
const homeDir = os.homedir();
|
|
17
|
+
const launchAgentsDir = path.join(homeDir, 'Library', 'LaunchAgents');
|
|
18
|
+
const plistPath = path.join(launchAgentsDir, 'com.aiva.tasks.plist');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(launchAgentsDir)) {
|
|
21
|
+
fs.mkdirSync(launchAgentsDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
25
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
26
|
+
<plist version="1.0">
|
|
27
|
+
<dict>
|
|
28
|
+
<key>Label</key>
|
|
29
|
+
<string>com.aiva.tasks</string>
|
|
30
|
+
<key>ProgramArguments</key>
|
|
31
|
+
<array>
|
|
32
|
+
<string>/opt/homebrew/bin/node</string>
|
|
33
|
+
<string>server.js</string>
|
|
34
|
+
</array>
|
|
35
|
+
<key>WorkingDirectory</key>
|
|
36
|
+
<string>${appDir}</string>
|
|
37
|
+
<key>RunAtLoad</key>
|
|
38
|
+
<true/>
|
|
39
|
+
<key>KeepAlive</key>
|
|
40
|
+
<true/>
|
|
41
|
+
<key>EnvironmentVariables</key>
|
|
42
|
+
<dict>
|
|
43
|
+
<key>CODING_ENABLED</key>
|
|
44
|
+
<string>false</string>
|
|
45
|
+
<key>AIVA_CHANNEL_PLUGIN</key>
|
|
46
|
+
<string>true</string>
|
|
47
|
+
<key>NODE_ENV</key>
|
|
48
|
+
<string>production</string>
|
|
49
|
+
<key>PATH</key>
|
|
50
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
51
|
+
</dict>
|
|
52
|
+
<key>StandardOutPath</key>
|
|
53
|
+
<string>/tmp/aiva-app.log</string>
|
|
54
|
+
<key>StandardErrorPath</key>
|
|
55
|
+
<string>/tmp/aiva-app-err.log</string>
|
|
56
|
+
</dict>
|
|
57
|
+
</plist>`;
|
|
58
|
+
|
|
59
|
+
fs.writeFileSync(plistPath, plist);
|
|
60
|
+
ok(`LaunchAgent written: ${plistPath}`);
|
|
61
|
+
|
|
62
|
+
// Load the LaunchAgent (bootout first if already loaded)
|
|
63
|
+
const uid = process.getuid ? process.getuid() : 501;
|
|
64
|
+
try {
|
|
65
|
+
execSync(`launchctl bootout gui/${uid}/com.aiva.tasks 2>/dev/null || true`, { stdio: 'pipe' });
|
|
66
|
+
} catch { /* not loaded, fine */ }
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
execSync(`launchctl bootstrap gui/${uid} "${plistPath}"`, { stdio: 'pipe' });
|
|
70
|
+
ok('LaunchAgent loaded');
|
|
71
|
+
} catch (e) {
|
|
72
|
+
warn(`LaunchAgent load failed: ${e.message}`);
|
|
73
|
+
info(`Load manually: launchctl bootstrap gui/${uid} "${plistPath}"`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return plistPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set up voice notes cron job on the device's own OpenClaw instance
|
|
81
|
+
*/
|
|
82
|
+
function setupVoiceNotesCron() {
|
|
83
|
+
section('Voice Notes Cron');
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Check if cron already exists
|
|
87
|
+
const existing = execSync('openclaw cron list 2>/dev/null || echo ""', { stdio: 'pipe' }).toString();
|
|
88
|
+
if (existing.includes('nightly-voice-notes')) {
|
|
89
|
+
ok('Voice notes cron already configured');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
execSync(`openclaw cron add \
|
|
94
|
+
--name nightly-voice-notes-processing \
|
|
95
|
+
--cron "0 23 * * *" \
|
|
96
|
+
--tz America/Los_Angeles \
|
|
97
|
+
--agent main \
|
|
98
|
+
--channel heartbeat \
|
|
99
|
+
--session isolated \
|
|
100
|
+
--no-deliver \
|
|
101
|
+
--timeout-seconds 300 \
|
|
102
|
+
--message "Process all pending voice notes from today. Fetch from localhost:3847/api/notes?status=pending. Classify each note (COMMAND/INFORMATION/TASK_REQUEST/JUNK). For COMMANDS, schedule as 8 AM systemEvents. For INFORMATION, file to memory. For TASK_REQUESTS, add to task board. Mark notes as processed. Send summary to device owner."`, {
|
|
103
|
+
stdio: 'pipe'
|
|
104
|
+
});
|
|
105
|
+
ok('Voice notes cron configured (11:00 PM PT)');
|
|
106
|
+
} catch (e) {
|
|
107
|
+
warn(`Voice notes cron setup failed: ${e.message}`);
|
|
108
|
+
info('Set up manually after OpenClaw is running');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { setupLaunchAgent, setupVoiceNotesCron };
|