@robbiesrobotics/alice-agents 1.5.2 → 1.5.4
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/alice-cloud.cjs +369 -0
- package/lib/installer.mjs +140 -38
- package/lib/mission-control.mjs +61 -0
- package/lib/prompter.mjs +20 -14
- package/package.json +3 -2
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* alice-cloud - Zero-touch client onboarding CLI for A.L.I.C.E. | Control
|
|
4
|
+
* Usage: alice-cloud <command>
|
|
5
|
+
* --non-interactive Skip interactive prompts (use env vars)
|
|
6
|
+
* ALICE_SUPABASE_TOKEN Supabase access token (non-interactive mode)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
|
+
|
|
14
|
+
const CONFIG_DIR = path.join(os.homedir(), '.openclaw');
|
|
15
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'alice-cloud.json');
|
|
16
|
+
const API_BASE = 'https://alice.av3.ai/api/cloud';
|
|
17
|
+
|
|
18
|
+
const NON_INTERACTIVE = process.argv.includes('--non-interactive') || process.env.ALICE_NON_INTERACTIVE === '1';
|
|
19
|
+
const SUPABASE_TOKEN = process.env.ALICE_SUPABASE_TOKEN;
|
|
20
|
+
|
|
21
|
+
function loadConfig() {
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
24
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
} catch (_) {}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveConfig(config) {
|
|
31
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
32
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function sleep(ms) {
|
|
36
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runCommand(cmd, args) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const child = spawn(cmd, args, { shell: true });
|
|
42
|
+
let stdout = '', stderr = '';
|
|
43
|
+
child.stdout.on('data', (d) => { stdout += d; });
|
|
44
|
+
child.stderr.on('data', (d) => { stderr += d; });
|
|
45
|
+
child.on('close', (code) => {
|
|
46
|
+
if (code === 0) resolve(stdout);
|
|
47
|
+
else reject(new Error(stderr || `exit ${code}`));
|
|
48
|
+
});
|
|
49
|
+
child.on('error', reject);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Login ──────────────────────────────────────────────────────────────────────
|
|
54
|
+
async function login(args) {
|
|
55
|
+
const open = (await import('open')).default;
|
|
56
|
+
const got = (await import('got')).default;
|
|
57
|
+
|
|
58
|
+
// Non-interactive mode: use token from env
|
|
59
|
+
if (NON_INTERACTIVE) {
|
|
60
|
+
const token = SUPABASE_TOKEN;
|
|
61
|
+
if (!token) {
|
|
62
|
+
console.error('❌ ALICE_SUPABASE_TOKEN env var required for non-interactive login.');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const res = await got(`${process.env.ALICE_SUPABASE_URL || 'https://xxxgvtwnlbtdgmlgccee.supabase.co'}/auth/v1/user`, {
|
|
67
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
68
|
+
throwHttpErrors: false,
|
|
69
|
+
}).json();
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
config.supabaseToken = token;
|
|
72
|
+
config.user = res;
|
|
73
|
+
saveConfig(config);
|
|
74
|
+
console.log('✅ Logged in as', res.email || res.user_metadata?.user_name || 'unknown');
|
|
75
|
+
return;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('❌ Token validation failed:', err.message);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('🔐 A.L.I.C.E. Cloud login');
|
|
83
|
+
console.log(' Opening browser for Supabase OAuth…');
|
|
84
|
+
|
|
85
|
+
const supabaseUrl = process.env.ALICE_SUPABASE_URL || 'https://xxxgvtwnlbtdgmlgccee.supabase.co';
|
|
86
|
+
const redirectUri = `${API_BASE}/auth/callback`;
|
|
87
|
+
const oauthUrl = `${supabaseUrl}/auth/v1/authorize?provider=github&redirect_to=${encodeURIComponent(redirectUri)}`;
|
|
88
|
+
|
|
89
|
+
await open(oauthUrl);
|
|
90
|
+
console.log(' Browser opened. Complete login in your browser.');
|
|
91
|
+
console.log(' After login, paste your Supabase access token here:');
|
|
92
|
+
process.stdout.write(' > ');
|
|
93
|
+
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
let token = '';
|
|
96
|
+
process.stdin.setEncoding('utf8');
|
|
97
|
+
process.stdin.on('data', (d) => { token += d; });
|
|
98
|
+
process.stdin.on('end', async () => {
|
|
99
|
+
token = token.trim();
|
|
100
|
+
if (!token) { reject(new Error('No token provided')); return; }
|
|
101
|
+
try {
|
|
102
|
+
const res = await got(`${supabaseUrl}/auth/v1/user`, {
|
|
103
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
104
|
+
throwHttpErrors: false,
|
|
105
|
+
}).json();
|
|
106
|
+
const config = loadConfig();
|
|
107
|
+
config.supabaseToken = token;
|
|
108
|
+
config.user = res;
|
|
109
|
+
saveConfig(config);
|
|
110
|
+
console.log(' ✅ Logged in as', res.email || res.user_metadata?.user_name || 'unknown');
|
|
111
|
+
resolve();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
reject(new Error('Token validation failed: ' + err.message));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Register ───────────────────────────────────────────────────────────────────
|
|
120
|
+
async function detectGatewayUrl() {
|
|
121
|
+
const got = (await import('got')).default;
|
|
122
|
+
const candidates = ['https://localhost:18789', 'http://localhost:18789'];
|
|
123
|
+
for (const url of candidates) {
|
|
124
|
+
try {
|
|
125
|
+
await got.get(url, { throwHttpErrors: true, timeout: { request: 2000 }, retry: { limit: 0 } });
|
|
126
|
+
return url;
|
|
127
|
+
} catch (_) {}
|
|
128
|
+
}
|
|
129
|
+
return 'https://localhost:18789';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readGatewayToken() {
|
|
133
|
+
const cfg = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
134
|
+
try {
|
|
135
|
+
const data = JSON.parse(fs.readFileSync(cfg, 'utf8'));
|
|
136
|
+
return data.gateway?.auth?.token || '';
|
|
137
|
+
} catch (_) {}
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function getOpenClawVersion() {
|
|
142
|
+
try { return (await runCommand('openclaw', ['--version'])).trim(); } catch (_) {}
|
|
143
|
+
return 'unknown';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function register(args) {
|
|
147
|
+
const got = (await import('got')).default;
|
|
148
|
+
const config = loadConfig();
|
|
149
|
+
|
|
150
|
+
if (!config.supabaseToken) {
|
|
151
|
+
console.error('❌ Not logged in. Run `alice-cloud login` first.');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const gatewayUrl = await detectGatewayUrl();
|
|
156
|
+
const gatewayToken = await readGatewayToken();
|
|
157
|
+
const hostname = os.hostname();
|
|
158
|
+
const version = await getOpenClawVersion();
|
|
159
|
+
|
|
160
|
+
console.log('🔗 Registering gateway with A.L.I.C.E. Cloud…');
|
|
161
|
+
console.log(` Gateway URL : ${gatewayUrl}`);
|
|
162
|
+
console.log(` Hostname : ${hostname}`);
|
|
163
|
+
console.log(` Version : ${version}`);
|
|
164
|
+
|
|
165
|
+
let lastError;
|
|
166
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
167
|
+
try {
|
|
168
|
+
const response = await got.post(`${API_BASE}/register`, {
|
|
169
|
+
json: { gatewayUrl, gatewayToken, hostname, version },
|
|
170
|
+
headers: { Authorization: `Bearer ${config.supabaseToken}` },
|
|
171
|
+
throwHttpErrors: true,
|
|
172
|
+
}).json();
|
|
173
|
+
config.registration = { gatewayUrl, hostname, version, registeredAt: new Date().toISOString() };
|
|
174
|
+
saveConfig(config);
|
|
175
|
+
console.log(' ✅ Gateway registered!');
|
|
176
|
+
return;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
lastError = err;
|
|
179
|
+
const delay = attempt * 2000;
|
|
180
|
+
console.log(` ⚠️ Attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms…`);
|
|
181
|
+
await sleep(delay);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
console.error('❌ Registration failed after 3 attempts:', lastError.message);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Status ─────────────────────────────────────────────────────────────────────
|
|
189
|
+
async function status(args) {
|
|
190
|
+
const got = (await import('got')).default;
|
|
191
|
+
const config = loadConfig();
|
|
192
|
+
|
|
193
|
+
if (!config.supabaseToken) {
|
|
194
|
+
console.error('❌ Not logged in. Run `alice-cloud login` first.');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const data = await got.get(`${API_BASE}/status`, {
|
|
200
|
+
headers: { Authorization: `Bearer ${config.supabaseToken}` },
|
|
201
|
+
throwHttpErrors: false,
|
|
202
|
+
}).json();
|
|
203
|
+
|
|
204
|
+
if (!data.registered) {
|
|
205
|
+
console.log('⚪ No gateway registered.');
|
|
206
|
+
console.log(' Run `alice-cloud register` to connect your gateway.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log('🟢 A.L.I.C.E. Cloud Status');
|
|
211
|
+
console.log(` Gateway URL : ${data.gatewayUrl}`);
|
|
212
|
+
console.log(` Hostname : ${data.hostname}`);
|
|
213
|
+
console.log(` Status : ${data.connected ? '🟢 Connected' : '🔴 Disconnected'}`);
|
|
214
|
+
console.log(` Last heartbeat: ${data.lastHeartbeat ? new Date(data.lastHeartbeat).toLocaleString() : 'Never'}`);
|
|
215
|
+
console.log(` Version : ${data.version}`);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
if (err.response?.statusCode === 401) {
|
|
218
|
+
console.error('❌ Session expired. Run `alice-cloud login` again.');
|
|
219
|
+
} else {
|
|
220
|
+
console.error('❌ Status check failed:', err.message);
|
|
221
|
+
}
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Unregister ─────────────────────────────────────────────────────────────────
|
|
227
|
+
async function unregister(args) {
|
|
228
|
+
const got = (await import('got')).default;
|
|
229
|
+
const config = loadConfig();
|
|
230
|
+
|
|
231
|
+
if (!config.supabaseToken) {
|
|
232
|
+
console.error('❌ Not logged in. Run `alice-cloud login` first.');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log('🗑️ Unregistering gateway…');
|
|
237
|
+
try {
|
|
238
|
+
await got.delete(`${API_BASE}/status`, {
|
|
239
|
+
headers: { Authorization: `Bearer ${config.supabaseToken}` },
|
|
240
|
+
throwHttpErrors: false,
|
|
241
|
+
});
|
|
242
|
+
delete config.registration;
|
|
243
|
+
saveConfig(config);
|
|
244
|
+
console.log(' ✅ Gateway unregistered.');
|
|
245
|
+
} catch (err) {
|
|
246
|
+
if (err.response?.statusCode === 404) {
|
|
247
|
+
console.log(' ℹ️ No gateway was registered.');
|
|
248
|
+
} else {
|
|
249
|
+
console.error('❌ Unregister failed:', err.message);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Watch (daemon) ─────────────────────────────────────────────────────────────
|
|
256
|
+
async function checkGatewayUp(gatewayUrl) {
|
|
257
|
+
const got = (await import('got')).default;
|
|
258
|
+
try {
|
|
259
|
+
await got.get(gatewayUrl, { throwHttpErrors: true, timeout: { request: 3000 }, retry: { limit: 0 } });
|
|
260
|
+
return true;
|
|
261
|
+
} catch (_) {}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function startHeartbeatLoop(config) {
|
|
266
|
+
const got = (await import('got')).default;
|
|
267
|
+
const gatewayUrl = config.registration?.gatewayUrl || 'https://localhost:18789';
|
|
268
|
+
|
|
269
|
+
async function heartbeat() {
|
|
270
|
+
try {
|
|
271
|
+
const isUp = await checkGatewayUp(gatewayUrl);
|
|
272
|
+
if (!isUp) {
|
|
273
|
+
console.log('[watch] Gateway down, attempting restart…');
|
|
274
|
+
try { await runCommand('openclaw', ['gateway', 'start']); await sleep(3000); } catch (_) {}
|
|
275
|
+
}
|
|
276
|
+
await got.post(`${API_BASE}/heartbeat`, {
|
|
277
|
+
json: { hostname: os.hostname(), connected: await checkGatewayUp(gatewayUrl), timestamp: new Date().toISOString() },
|
|
278
|
+
headers: { Authorization: `Bearer ${config.supabaseToken}` },
|
|
279
|
+
throwHttpErrors: false, timeout: { request: 10000 },
|
|
280
|
+
});
|
|
281
|
+
console.log(`[${new Date().toLocaleTimeString()}] heartbeat sent (gateway: ${isUp ? 'up' : 'down'})`);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error('[watch] heartbeat error:', err.message);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await heartbeat();
|
|
288
|
+
const interval = setInterval(heartbeat, 30_000);
|
|
289
|
+
process.on('SIGINT', () => { clearInterval(interval); process.exit(0); });
|
|
290
|
+
process.on('SIGTERM', () => { clearInterval(interval); process.exit(0); });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function watch(args) {
|
|
294
|
+
const config = loadConfig();
|
|
295
|
+
if (!config.supabaseToken) {
|
|
296
|
+
console.error('❌ Not logged in. Run `alice-cloud login` first.');
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (args.daemon || process.argv.includes('--daemon')) {
|
|
301
|
+
const child = spawn(process.execPath, [__filename, 'watch', '--running'], {
|
|
302
|
+
detached: true, stdio: 'ignore', env: { ...process.env, ALICE_NON_INTERACTIVE: '1' },
|
|
303
|
+
});
|
|
304
|
+
child.unref();
|
|
305
|
+
console.log('🚀 alice-cloud watch started in background (PID:', child.pid, ')');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log('👁️ A.L.I.C.E. Cloud watch daemon started');
|
|
310
|
+
console.log(' Press Ctrl+C to stop.');
|
|
311
|
+
await startHeartbeatLoop(config);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Programmatic API exports ───────────────────────────────────────────────────
|
|
315
|
+
// Allow ESM callers to import these via createRequire or spawn as child process.
|
|
316
|
+
module.exports = {
|
|
317
|
+
login,
|
|
318
|
+
register,
|
|
319
|
+
status,
|
|
320
|
+
unregister,
|
|
321
|
+
watch,
|
|
322
|
+
loadConfig,
|
|
323
|
+
saveConfig,
|
|
324
|
+
detectGatewayUrl,
|
|
325
|
+
readGatewayToken,
|
|
326
|
+
API_BASE,
|
|
327
|
+
CONFIG_FILE,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// ── CLI dispatcher ─────────────────────────────────────────────────────────────
|
|
331
|
+
// Only run as CLI when this file is the main module (not when required/imported)
|
|
332
|
+
if (require.main === module) {
|
|
333
|
+
const commands = { login, register, status, unregister, watch };
|
|
334
|
+
const cmd = process.argv[2];
|
|
335
|
+
|
|
336
|
+
if (!cmd) {
|
|
337
|
+
console.log(`alice-cloud v1.0.1 — A.L.I.C.E. | Control Cloud CLI
|
|
338
|
+
|
|
339
|
+
Usage: alice-cloud <command> [options]
|
|
340
|
+
|
|
341
|
+
Commands:
|
|
342
|
+
login Authenticate with Supabase
|
|
343
|
+
register Register this gateway with A.L.I.C.E. Cloud
|
|
344
|
+
status Show gateway connection status
|
|
345
|
+
unregister Remove gateway registration
|
|
346
|
+
watch Start background heartbeat daemon
|
|
347
|
+
|
|
348
|
+
Options:
|
|
349
|
+
--non-interactive Use env vars (ALICE_SUPABASE_TOKEN) instead of prompts
|
|
350
|
+
|
|
351
|
+
Environment:
|
|
352
|
+
ALICE_SUPABASE_TOKEN Supabase access token (for non-interactive mode)
|
|
353
|
+
ALICE_SUPABASE_URL Supabase project URL (default: xxx project)
|
|
354
|
+
|
|
355
|
+
Run 'alice-cloud <command> --help' for more options.`);
|
|
356
|
+
process.exit(0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const handler = commands[cmd];
|
|
360
|
+
if (!handler) {
|
|
361
|
+
console.error(`Unknown command: ${cmd}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
handler(process.argv.slice(3)).catch((err) => {
|
|
366
|
+
console.error('Error:', err.message);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
});
|
|
369
|
+
}
|
package/lib/installer.mjs
CHANGED
|
@@ -6,7 +6,14 @@ import { homedir } from 'node:os';
|
|
|
6
6
|
import { configExists, mergeConfig, removeAliceAgents, detectAvailableModels } from './config-merger.mjs';
|
|
7
7
|
import { scaffoldAll } from './workspace-scaffolder.mjs';
|
|
8
8
|
import { readManifest, writeManifest } from './manifest.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
configureMissionControlCloud,
|
|
11
|
+
getDefaultMissionControlSettings,
|
|
12
|
+
getCloudStatus,
|
|
13
|
+
isCloudAuthenticated,
|
|
14
|
+
isCloudRegistered,
|
|
15
|
+
configureCloudFromSupabase,
|
|
16
|
+
} from './mission-control.mjs';
|
|
10
17
|
import {
|
|
11
18
|
promptInstallMode,
|
|
12
19
|
promptUserInfo,
|
|
@@ -15,8 +22,10 @@ import {
|
|
|
15
22
|
promptTier,
|
|
16
23
|
promptLicenseKey,
|
|
17
24
|
promptCloudAddon,
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
promptCloudSetup,
|
|
26
|
+
promptCloudReauth,
|
|
27
|
+
promptCloudHeartbeat,
|
|
28
|
+
promptCloudToken,
|
|
20
29
|
confirm,
|
|
21
30
|
choose,
|
|
22
31
|
input,
|
|
@@ -470,11 +479,10 @@ function printSummaryWithOptions(mode, tier, agents, preset, userInfo, detectedM
|
|
|
470
479
|
`${dim('Model:')} ${green(modelLabel)}`,
|
|
471
480
|
`${dim('User:')} ${green(userInfo.name)}`,
|
|
472
481
|
`${dim('Timezone:')} ${green(userInfo.timezone)}`,
|
|
473
|
-
`${dim('
|
|
482
|
+
`${dim('Cloud:')} ${green(missionControl?.enabled ? 'enabled' : 'local only')}`,
|
|
474
483
|
...(missionControl?.enabled
|
|
475
484
|
? [
|
|
476
485
|
`${dim('Dashboard:')} ${green(missionControl.dashboardUrl)}`,
|
|
477
|
-
`${dim('Ingest:')} ${green(missionControl.ingestUrl)}`,
|
|
478
486
|
]
|
|
479
487
|
: []),
|
|
480
488
|
'',
|
|
@@ -486,6 +494,75 @@ function printSummaryWithOptions(mode, tier, agents, preset, userInfo, detectedM
|
|
|
486
494
|
console.log('');
|
|
487
495
|
}
|
|
488
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Run inline cloud onboarding — opens browser, validates token, registers gateway.
|
|
499
|
+
* Uses alice-cloud CLI spawned as a child process (CJS ↔ ESM boundary).
|
|
500
|
+
*/
|
|
501
|
+
async function _runCloudOnboarding(auto, options, existingMissionControl) {
|
|
502
|
+
const { execSync: execSyncLocal, spawn: spawnLocal } = await import('node:child_process');
|
|
503
|
+
const aliceCloudBin = join(__dirname, '..', 'bin', 'alice-cloud.cjs');
|
|
504
|
+
const defaults = getDefaultMissionControlSettings();
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
// Step 1: Login — open browser for Supabase OAuth
|
|
508
|
+
console.log('');
|
|
509
|
+
console.log(` ${icons.pkg} ${bold('Setting up A.L.I.C.E. Cloud...')}`);
|
|
510
|
+
console.log(` Opening your browser to sign in with GitHub...`);
|
|
511
|
+
console.log('');
|
|
512
|
+
|
|
513
|
+
// Spawn alice-cloud login as a child process
|
|
514
|
+
// In non-interactive mode, use env var for token
|
|
515
|
+
if (auto && options.cloudSupabaseToken) {
|
|
516
|
+
execSyncLocal(
|
|
517
|
+
`node ${JSON.stringify(aliceCloudBin)} login --non-interactive`,
|
|
518
|
+
{
|
|
519
|
+
stdio: 'inherit',
|
|
520
|
+
env: { ...process.env, ALICE_SUPABASE_TOKEN: options.cloudSupabaseToken, ALICE_NON_INTERACTIVE: '1' },
|
|
521
|
+
}
|
|
522
|
+
);
|
|
523
|
+
} else if (!auto) {
|
|
524
|
+
// Interactive: spawn login which opens browser and waits for token paste
|
|
525
|
+
execSyncLocal(`node ${JSON.stringify(aliceCloudBin)} login`, { stdio: 'inherit' });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Step 2: Register gateway
|
|
529
|
+
if (isCloudAuthenticated()) {
|
|
530
|
+
console.log('');
|
|
531
|
+
console.log(` ${icons.pkg} ${bold('Registering gateway with A.L.I.C.E. Cloud...')}`);
|
|
532
|
+
execSyncLocal(`node ${JSON.stringify(aliceCloudBin)} register`, { stdio: 'inherit' });
|
|
533
|
+
printStepDone('Cloud login and registration complete');
|
|
534
|
+
|
|
535
|
+
// Step 3: Ask about heartbeat daemon
|
|
536
|
+
const startHeartbeat = auto ? false : await promptCloudHeartbeat();
|
|
537
|
+
if (startHeartbeat) {
|
|
538
|
+
console.log(` ${dim('Starting heartbeat daemon...')}`);
|
|
539
|
+
execSyncLocal(`node ${JSON.stringify(aliceCloudBin)} watch --daemon`, { stdio: 'inherit' });
|
|
540
|
+
printStepDone('Heartbeat daemon started');
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
printStepSkip('Cloud registration', 'login was skipped or failed — run `alice-cloud login` later');
|
|
544
|
+
}
|
|
545
|
+
} catch (err) {
|
|
546
|
+
console.log(` ${icons.warn} ${yellow('Cloud setup encountered an issue: ' + (err.message || 'unknown error'))}`);
|
|
547
|
+
console.log(` ${dim('You can complete setup later with:')} ${cyan('alice-cloud login && alice-cloud register')}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Build missionControl object from whatever state we achieved
|
|
551
|
+
const cloudStatus = getCloudStatus();
|
|
552
|
+
return {
|
|
553
|
+
enabled: true,
|
|
554
|
+
provider: 'cloud',
|
|
555
|
+
dashboardUrl: existingMissionControl?.dashboardUrl || defaults.dashboardUrl,
|
|
556
|
+
ingestUrl: existingMissionControl?.ingestUrl || `${defaults.dashboardUrl}/api/v1/ingest`,
|
|
557
|
+
sourceNode: existingMissionControl?.sourceNode || defaults.sourceNode,
|
|
558
|
+
teamId: String(options.cloudTeamId || existingMissionControl?.teamId || '').trim(),
|
|
559
|
+
teamSlug: String(options.cloudTeamSlug || existingMissionControl?.teamSlug || '').trim(),
|
|
560
|
+
teamName: String(options.cloudTeamName || existingMissionControl?.teamName || '').trim(),
|
|
561
|
+
teamPlan: String(options.cloudTeamPlan || existingMissionControl?.teamPlan || '').trim(),
|
|
562
|
+
...(cloudStatus.authenticated ? { hasIngestToken: true } : {}),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
489
566
|
export async function runInstall(options = {}) {
|
|
490
567
|
const auto = options.yes || false;
|
|
491
568
|
const manifest = readManifest();
|
|
@@ -718,36 +795,61 @@ export async function runInstall(options = {}) {
|
|
|
718
795
|
: cloudFlag ?? await promptCloudAddon();
|
|
719
796
|
|
|
720
797
|
if (enableCloud) {
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
798
|
+
const cloudStatus = getCloudStatus();
|
|
799
|
+
|
|
800
|
+
// Upgrade mode: show existing cloud status
|
|
801
|
+
if (mode === 'upgrade' && (cloudStatus.authenticated || existingMissionControl?.enabled)) {
|
|
802
|
+
console.log('');
|
|
803
|
+
console.log(` ${icons.ok} ${green('A.L.I.C.E. Cloud is configured')}`);
|
|
804
|
+
if (cloudStatus.user) {
|
|
805
|
+
console.log(` ${dim('User:')} ${cloudStatus.user}`);
|
|
806
|
+
}
|
|
807
|
+
if (cloudStatus.gatewayUrl) {
|
|
808
|
+
console.log(` ${dim('Gateway:')} ${cloudStatus.gatewayUrl}`);
|
|
809
|
+
}
|
|
810
|
+
if (cloudStatus.registeredAt) {
|
|
811
|
+
console.log(` ${dim('Registered:')} ${cloudStatus.registeredAt}`);
|
|
812
|
+
}
|
|
813
|
+
console.log('');
|
|
814
|
+
|
|
815
|
+
const reauth = auto ? false : await promptCloudReauth();
|
|
816
|
+
if (!reauth) {
|
|
817
|
+
// Keep existing config — use Supabase config if available, fall back to legacy
|
|
818
|
+
if (cloudStatus.authenticated) {
|
|
819
|
+
missionControl = {
|
|
820
|
+
enabled: true,
|
|
821
|
+
provider: 'cloud',
|
|
822
|
+
dashboardUrl: existingMissionControl?.dashboardUrl || 'https://alice.av3.ai',
|
|
823
|
+
ingestUrl: existingMissionControl?.ingestUrl || 'https://alice.av3.ai/api/v1/ingest',
|
|
824
|
+
sourceNode: existingMissionControl?.sourceNode || getDefaultMissionControlSettings().sourceNode,
|
|
825
|
+
};
|
|
826
|
+
} else {
|
|
827
|
+
missionControl = { ...existingMissionControl, enabled: true };
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
// Re-authenticate — run alice-cloud login inline
|
|
831
|
+
missionControl = await _runCloudOnboarding(auto, options, existingMissionControl);
|
|
832
|
+
}
|
|
833
|
+
} else {
|
|
834
|
+
// Fresh install — run cloud onboarding
|
|
835
|
+
const wantsSetup = auto ? true : await promptCloudSetup();
|
|
836
|
+
|
|
837
|
+
if (wantsSetup) {
|
|
838
|
+
missionControl = await _runCloudOnboarding(auto, options, existingMissionControl);
|
|
839
|
+
} else {
|
|
840
|
+
console.log('');
|
|
841
|
+
console.log(` ${dim('You can set up cloud later with:')} ${cyan('alice-cloud login && alice-cloud register')}`);
|
|
842
|
+
console.log('');
|
|
843
|
+
// Cloud addon enabled but setup skipped — bridge installed, no auth yet
|
|
844
|
+
missionControl = {
|
|
845
|
+
enabled: true,
|
|
846
|
+
provider: 'cloud',
|
|
847
|
+
dashboardUrl: 'https://alice.av3.ai',
|
|
848
|
+
ingestUrl: 'https://alice.av3.ai/api/v1/ingest',
|
|
849
|
+
sourceNode: getDefaultMissionControlSettings().sourceNode,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
}
|
|
751
853
|
}
|
|
752
854
|
}
|
|
753
855
|
|
|
@@ -790,12 +892,12 @@ export async function runInstall(options = {}) {
|
|
|
790
892
|
|
|
791
893
|
if (missionControl?.enabled) {
|
|
792
894
|
const missionControlResult = configureMissionControlCloud(missionControl);
|
|
793
|
-
printStepDone('
|
|
895
|
+
printStepDone('A.L.I.C.E. Cloud', missionControlResult.summary.dashboardUrl);
|
|
794
896
|
if (!missionControlResult.summary.hasIngestToken) {
|
|
795
|
-
printStepSkip('Cloud
|
|
897
|
+
printStepSkip('Cloud authentication pending', 'run `alice-cloud login && alice-cloud register` to complete setup');
|
|
796
898
|
}
|
|
797
899
|
} else {
|
|
798
|
-
printStepSkip('
|
|
900
|
+
printStepSkip('A.L.I.C.E. Cloud', 'not enabled for this install');
|
|
799
901
|
}
|
|
800
902
|
|
|
801
903
|
// Scaffold workspaces
|
package/lib/mission-control.mjs
CHANGED
|
@@ -179,3 +179,64 @@ export function getDefaultMissionControlSettings() {
|
|
|
179
179
|
export function hasMissionControlBridgeInstalled() {
|
|
180
180
|
return existsSync(join(OPENCLAW_HOME, 'extensions', BRIDGE_ID));
|
|
181
181
|
}
|
|
182
|
+
|
|
183
|
+
// ── Supabase-based cloud onboarding helpers ────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
const CLOUD_CONFIG_PATH = join(OPENCLAW_HOME, 'alice-cloud.json');
|
|
186
|
+
|
|
187
|
+
export function readCloudConfig() {
|
|
188
|
+
return readJsonFile(CLOUD_CONFIG_PATH);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function isCloudAuthenticated() {
|
|
192
|
+
const config = readCloudConfig();
|
|
193
|
+
return !!(config && config.supabaseToken);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function isCloudRegistered() {
|
|
197
|
+
const config = readCloudConfig();
|
|
198
|
+
return !!(config && config.registration);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function getCloudStatus() {
|
|
202
|
+
const config = readCloudConfig();
|
|
203
|
+
if (!config) return { authenticated: false, registered: false };
|
|
204
|
+
return {
|
|
205
|
+
authenticated: !!config.supabaseToken,
|
|
206
|
+
registered: !!config.registration,
|
|
207
|
+
user: config.user?.email || config.user?.user_metadata?.user_name || null,
|
|
208
|
+
gatewayUrl: config.registration?.gatewayUrl || null,
|
|
209
|
+
hostname: config.registration?.hostname || null,
|
|
210
|
+
registeredAt: config.registration?.registeredAt || null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Configure cloud using Supabase auth (new flow).
|
|
216
|
+
* This preserves the existing bridge installation and MC config writing,
|
|
217
|
+
* and reads auth state from alice-cloud.json.
|
|
218
|
+
*/
|
|
219
|
+
export function configureCloudFromSupabase(input = {}) {
|
|
220
|
+
// Read alice-cloud.json for auth info
|
|
221
|
+
const cloudConfig = readCloudConfig() || {};
|
|
222
|
+
|
|
223
|
+
// Build settings — we still use the same bridge + MC config infrastructure,
|
|
224
|
+
// but tokens now come from Supabase auth rather than manual ingest tokens
|
|
225
|
+
const settingsInput = {
|
|
226
|
+
dashboardUrl: input.dashboardUrl || DEFAULT_DASHBOARD_URL,
|
|
227
|
+
sourceNode: input.sourceNode || hostname() || 'openclaw-local',
|
|
228
|
+
teamId: input.teamId || '',
|
|
229
|
+
teamSlug: input.teamSlug || '',
|
|
230
|
+
teamName: input.teamName || '',
|
|
231
|
+
teamPlan: input.teamPlan || '',
|
|
232
|
+
enabled: true,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// If we have a supabase token, use it as the ingest/worker token for bridge compatibility
|
|
236
|
+
if (cloudConfig.supabaseToken) {
|
|
237
|
+
settingsInput.ingestToken = cloudConfig.supabaseToken;
|
|
238
|
+
settingsInput.workerToken = cloudConfig.supabaseToken;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return configureMissionControlCloud(settingsInput);
|
|
242
|
+
}
|
package/lib/prompter.mjs
CHANGED
|
@@ -128,27 +128,33 @@ export async function promptLicenseKey() {
|
|
|
128
128
|
|
|
129
129
|
export async function promptCloudAddon() {
|
|
130
130
|
console.log('');
|
|
131
|
-
console.log('
|
|
132
|
-
console.log('
|
|
131
|
+
console.log(' A.L.I.C.E. Cloud add-on');
|
|
132
|
+
console.log(' Remote monitoring, fleet management, and cloud sync for Pro users.');
|
|
133
133
|
console.log('');
|
|
134
|
-
return confirm(' Enable
|
|
134
|
+
return confirm(' Enable A.L.I.C.E. Cloud on this machine?', true);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
export async function
|
|
137
|
+
export async function promptCloudSetup() {
|
|
138
138
|
console.log('');
|
|
139
|
-
console.log('
|
|
140
|
-
console.log('
|
|
141
|
-
console.log(' You can find it in the "A.L.I.C.E. Pro License Delivered" email.');
|
|
142
|
-
console.log(' Press Enter to skip — you can add it later.');
|
|
139
|
+
console.log(' A.L.I.C.E. Cloud connects your local gateway to alice.av3.ai');
|
|
140
|
+
console.log(' for remote monitoring, fleet management, and cloud sync.');
|
|
143
141
|
console.log('');
|
|
144
|
-
return
|
|
142
|
+
return confirm(' Would you like to set up A.L.I.C.E. Cloud now?', true);
|
|
145
143
|
}
|
|
146
144
|
|
|
147
|
-
export async function
|
|
148
|
-
if (!ingestToken) return '';
|
|
145
|
+
export async function promptCloudReauth() {
|
|
149
146
|
console.log('');
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
return confirm(' Re-authenticate with A.L.I.C.E. Cloud?', false);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function promptCloudHeartbeat() {
|
|
151
|
+
return confirm(' Start the heartbeat daemon (keeps cloud connection alive)?', true);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function promptCloudToken() {
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log(' After logging in via your browser, paste the access token here.');
|
|
157
|
+
console.log(' Press Enter to skip — you can run `alice-cloud login` later.');
|
|
152
158
|
console.log('');
|
|
153
|
-
return input('
|
|
159
|
+
return input(' Access token', '');
|
|
154
160
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robbiesrobotics/alice-agents",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"description": "A.L.I.C.E. \u2014 31 AI agents for OpenClaw. One conversation, one team.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"alice-agents": "bin/alice-install.mjs"
|
|
6
|
+
"alice-agents": "bin/alice-install.mjs",
|
|
7
|
+
"alice-cloud": "bin/alice-cloud.cjs"
|
|
7
8
|
},
|
|
8
9
|
"type": "module",
|
|
9
10
|
"engines": {
|