@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.
@@ -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 { configureMissionControlCloud, getDefaultMissionControlSettings } from './mission-control.mjs';
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
- promptMissionControlToken,
19
- promptMissionControlWorkerToken,
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('Mission Control:')} ${green(missionControl?.enabled ? 'cloud enabled' : 'local only')}`,
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 defaults = getDefaultMissionControlSettings();
722
- const dashboardUrl =
723
- String(options.cloudDashboardUrl || existingMissionControl?.dashboardUrl || defaults.dashboardUrl).trim();
724
- const ingestUrl =
725
- String(options.cloudIngestUrl || existingMissionControl?.ingestUrl || `${dashboardUrl}/api/v1/ingest`).trim();
726
- const ingestToken =
727
- auto
728
- ? String(options.cloudToken || '').trim()
729
- : String(options.cloudToken || await promptMissionControlToken()).trim();
730
- const workerToken =
731
- auto
732
- ? String(options.cloudWorkerToken || '').trim()
733
- : String(options.cloudWorkerToken || await promptMissionControlWorkerToken(ingestToken)).trim();
734
- const sourceNode =
735
- String(options.cloudSourceNode || existingMissionControl?.sourceNode || defaults.sourceNode).trim();
736
-
737
- missionControl = {
738
- enabled: true,
739
- provider: 'cloud',
740
- dashboardUrl,
741
- ingestUrl,
742
- sourceNode,
743
- teamId: String(options.cloudTeamId || existingMissionControl?.teamId || '').trim(),
744
- teamSlug: String(options.cloudTeamSlug || existingMissionControl?.teamSlug || '').trim(),
745
- teamName: String(options.cloudTeamName || existingMissionControl?.teamName || '').trim(),
746
- teamPlan: String(options.cloudTeamPlan || existingMissionControl?.teamPlan || '').trim(),
747
- hasIngestToken: !!ingestToken,
748
- ingestToken,
749
- workerToken,
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('Mission Control cloud', missionControlResult.summary.dashboardUrl);
895
+ printStepDone('A.L.I.C.E. Cloud', missionControlResult.summary.dashboardUrl);
794
896
  if (!missionControlResult.summary.hasIngestToken) {
795
- printStepSkip('Cloud access token not set', 'bridge installed; add token later to enable authenticated ingest');
897
+ printStepSkip('Cloud authentication pending', 'run `alice-cloud login && alice-cloud register` to complete setup');
796
898
  }
797
899
  } else {
798
- printStepSkip('Mission Control cloud', 'not enabled for this install');
900
+ printStepSkip('A.L.I.C.E. Cloud', 'not enabled for this install');
799
901
  }
800
902
 
801
903
  // Scaffold workspaces
@@ -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(' Mission Control Cloud add-on');
132
- console.log(' Hosted dashboard, telemetry sync, and cloud memory for Pro users.');
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 the Mission Control Cloud add-on on this machine?', true);
134
+ return confirm(' Enable A.L.I.C.E. Cloud on this machine?', true);
135
135
  }
136
136
 
137
- export async function promptMissionControlToken() {
137
+ export async function promptCloudSetup() {
138
138
  console.log('');
139
- console.log(' Enter your Mission Control Ingest Token from your purchase email.');
140
- console.log(' It looks like: mc_ingest_[64 hex chars]');
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 input(' Mission Control Ingest Token', '');
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 promptMissionControlWorkerToken(ingestToken = '') {
148
- if (!ingestToken) return '';
145
+ export async function promptCloudReauth() {
149
146
  console.log('');
150
- console.log(' Enter your Mission Control Worker Token from the same purchase email.');
151
- console.log(' It looks like: mc_worker_[64 hex chars]');
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(' Mission Control Worker Token', '');
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.2",
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": {