limbo-ai 1.15.0 → 1.17.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/cli.js CHANGED
@@ -1352,16 +1352,97 @@ ${c.green}${c.bold}╚═══════════════════
1352
1352
  console.log(` "${t(cfg.language, 'nonTelegramPrompt', gatewayToken)}"`);
1353
1353
  }
1354
1354
 
1355
+ // ─── Docker auto-install ──────────────────────────────────────────────────────
1356
+
1357
+ function installDocker() {
1358
+ const platform = os.platform();
1359
+ if (platform === 'linux') {
1360
+ header('Installing Docker...');
1361
+ try {
1362
+ execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit' });
1363
+ ok('Docker installed.');
1364
+ } catch {
1365
+ die('Failed to install Docker. Install it manually: https://docs.docker.com/get-docker/');
1366
+ }
1367
+ } else if (platform === 'darwin') {
1368
+ die(`Docker is required but not installed.
1369
+
1370
+ Install Docker Desktop for Mac:
1371
+ ${c.cyan}https://docs.docker.com/desktop/install/mac-install/${c.reset}
1372
+
1373
+ Then run ${c.bold}npx limbo-ai${c.reset} again.`);
1374
+ } else {
1375
+ die('Docker is required but not installed. See https://docs.docker.com/get-docker/');
1376
+ }
1377
+ }
1378
+
1379
+ function extractWizardUrl(maxAttempts = 15) {
1380
+ for (let i = 1; i <= maxAttempts; i++) {
1381
+ try {
1382
+ const logs = runDockerCompose(['logs', '--no-log-prefix'], {
1383
+ stdio: 'pipe',
1384
+ encoding: 'utf8',
1385
+ });
1386
+ const output = logs.stdout || '';
1387
+ const match = output.match(/SETUP_URL=(https?:\/\/\S+)/);
1388
+ if (match) return match[1];
1389
+ } catch {}
1390
+ log(`Waiting for setup wizard URL... (${i}/${maxAttempts})`);
1391
+ sleep(2000);
1392
+ }
1393
+ return null;
1394
+ }
1395
+
1396
+ function printWizardUrl(url) {
1397
+ const displayUrl = url.replace('0.0.0.0', '127.0.0.1');
1398
+ console.log(`
1399
+ ${c.green}${c.bold}╔════════════════════════════════════════════════════════╗${c.reset}
1400
+ ${c.green}${c.bold}║ Setup wizard is ready! ║${c.reset}
1401
+ ${c.green}${c.bold}╚════════════════════════════════════════════════════════╝${c.reset}
1402
+
1403
+ Open this URL to complete setup:
1404
+
1405
+ ${c.cyan}${c.bold}${displayUrl}${c.reset}
1406
+
1407
+ The wizard will guide you through provider, API key, and model selection.
1408
+ Once complete, Limbo will restart and be ready to use.
1409
+
1410
+ ${c.dim}Logs: limbo logs | Stop: limbo stop${c.reset}
1411
+ `);
1412
+ // Auto-open on macOS
1413
+ if (os.platform() === 'darwin') {
1414
+ try { execSync(`open "${displayUrl}"`, { stdio: 'pipe' }); } catch {}
1415
+ }
1416
+ }
1417
+
1418
+ function writeMinimalEnv() {
1419
+ ensureComposeFile(false);
1420
+ const gatewayToken = ensureGatewayToken({});
1421
+ const content = `CLI_LANGUAGE=en\nLIMBO_PORT=${PORT}\n`;
1422
+ fs.writeFileSync(ENV_FILE, content, { mode: 0o600 });
1423
+ // Ensure gateway token secret exists for compose
1424
+ writeSecretFile('gateway_token', gatewayToken);
1425
+ return gatewayToken;
1426
+ }
1427
+
1355
1428
  // ─── Commands ────────────────────────────────────────────────────────────────
1356
1429
 
1357
1430
  async function cmdStart() {
1358
- if (!hasDocker()) die(t('en', 'dockerMissing'));
1431
+ // ── Auto-install Docker if missing ────────────────────────────────────────
1432
+ if (!hasDocker()) {
1433
+ installDocker();
1434
+ // Verify it worked
1435
+ if (!hasDocker()) die(t('en', 'dockerMissing'));
1436
+ }
1359
1437
 
1360
1438
  const hardened = process.argv.includes('--hardened');
1439
+ const cliMode = process.argv.includes('--cli');
1440
+ const reconfig = process.argv.includes('--reconfigure');
1361
1441
 
1362
- // ── Detect existing OpenClaw ──────────────────────────────────────────────
1442
+ // ── Detect existing OpenClaw / port selection ─────────────────────────────
1363
1443
  const existingEnv = parseEnvFile();
1364
1444
  const alreadyHasEnv = fs.existsSync(ENV_FILE);
1445
+ const hasProviderConfig = alreadyHasEnv && existingEnv.MODEL_PROVIDER;
1365
1446
 
1366
1447
  if (existingEnv.LIMBO_PORT) {
1367
1448
  const parsed = parseInt(existingEnv.LIMBO_PORT, 10);
@@ -1385,10 +1466,8 @@ async function cmdStart() {
1385
1466
  }
1386
1467
 
1387
1468
  ensureComposeFile(hardened);
1388
- let cfg;
1389
- let lang = existingEnv.CLI_LANGUAGE || 'en';
1390
1469
 
1391
- // ── Headless mode ──────────────────────────────────────────────────────────
1470
+ // ── Route: Headless (--provider flag) ─────────────────────────────────────
1392
1471
  const flagProvider = parseFlag('--provider');
1393
1472
  const flagApiKey = parseFlag('--api-key');
1394
1473
  const flagModel = parseFlag('--model');
@@ -1403,13 +1482,13 @@ async function cmdStart() {
1403
1482
  die(t(flagLang, 'headlessMissingApiKey'));
1404
1483
  }
1405
1484
 
1406
- lang = flagLang;
1485
+ const lang = flagLang;
1407
1486
  const providerFamily = deriveProviderFamily(flagProvider);
1408
1487
  const catalog = getModelCatalog(providerFamily, 'api-key');
1409
1488
  const modelName = flagModel || catalog.defaultModel;
1410
1489
 
1411
1490
  log(t(lang, 'headlessStarting'));
1412
- cfg = {
1491
+ const cfg = {
1413
1492
  language: lang,
1414
1493
  authMode: 'api-key',
1415
1494
  provider: catalog.provider,
@@ -1423,34 +1502,89 @@ async function cmdStart() {
1423
1502
  };
1424
1503
  writeEnv({ ...cfg, CLI_LANGUAGE: cfg.language }, existingEnv);
1425
1504
  ok(t(cfg.language, 'envWritten'));
1426
- } else if (alreadyHasEnv) {
1427
- log(existingEnv.MODEL_PROVIDER ? t(lang, 'foundExistingConfig') : `Found existing config at ${ENV_FILE}`);
1428
- const reconfig = process.argv.includes('--reconfigure');
1429
- if (!reconfig) {
1430
- lang = existingEnv.CLI_LANGUAGE || 'en';
1431
- log(t(lang, 'reconfigureHint'));
1432
- ensureGatewayToken(existingEnv);
1433
- cfg = {
1434
- language: lang,
1435
- provider: existingEnv.MODEL_PROVIDER || 'anthropic',
1436
- providerFamily: deriveProviderFamily(existingEnv.MODEL_PROVIDER),
1437
- authMode: existingEnv.AUTH_MODE || 'api-key',
1438
- modelName: existingEnv.MODEL_NAME || 'claude-opus-4-6',
1439
- telegramEnabled: existingEnv.TELEGRAM_ENABLED || 'false',
1440
- };
1441
- } else {
1442
- header(t(lang, 'reconfiguration'));
1443
- cfg = await collectConfig(existingEnv);
1444
- writeEnv({ ...cfg, CLI_LANGUAGE: cfg.language }, existingEnv);
1445
- ok(t(cfg.language, 'envWritten'));
1446
- }
1447
- } else {
1448
- header(t('en', 'configuration'));
1449
- cfg = await collectConfig(existingEnv);
1505
+ return startContainerWithConfig(cfg, existingEnv, alreadyHasEnv);
1506
+ }
1507
+
1508
+ // ── Route: Existing config, no reconfigure ────────────────────────────────
1509
+ if (hasProviderConfig && !reconfig) {
1510
+ const lang = existingEnv.CLI_LANGUAGE || 'en';
1511
+ log(t(lang, 'foundExistingConfig'));
1512
+ log(t(lang, 'reconfigureHint'));
1513
+ ensureGatewayToken(existingEnv);
1514
+ const cfg = {
1515
+ language: lang,
1516
+ provider: existingEnv.MODEL_PROVIDER || 'anthropic',
1517
+ providerFamily: deriveProviderFamily(existingEnv.MODEL_PROVIDER),
1518
+ authMode: existingEnv.AUTH_MODE || 'api-key',
1519
+ modelName: existingEnv.MODEL_NAME || 'claude-opus-4-6',
1520
+ telegramEnabled: existingEnv.TELEGRAM_ENABLED || 'false',
1521
+ };
1522
+ return startContainerWithConfig(cfg, existingEnv, alreadyHasEnv);
1523
+ }
1524
+
1525
+ // ── Route: CLI prompts (--cli flag or --reconfigure --cli) ────────────────
1526
+ if (cliMode) {
1527
+ const lang = existingEnv.CLI_LANGUAGE || 'en';
1528
+ header(reconfig ? t(lang, 'reconfiguration') : t('en', 'configuration'));
1529
+ const cfg = await collectConfig(existingEnv);
1450
1530
  writeEnv({ ...cfg, CLI_LANGUAGE: cfg.language }, existingEnv);
1451
1531
  ok(t(cfg.language, 'envWritten'));
1532
+ return startContainerWithConfig(cfg, existingEnv, alreadyHasEnv);
1533
+ }
1534
+
1535
+ // ── Route: Wizard reconfigure (--reconfigure, no --cli) ───────────────────
1536
+ if (reconfig && hasProviderConfig) {
1537
+ log('Resetting configuration for setup wizard...');
1538
+ // Remove provider config from .env so container enters setup mode
1539
+ const minimalContent = `CLI_LANGUAGE=${existingEnv.CLI_LANGUAGE || 'en'}\nLIMBO_PORT=${PORT}\n`;
1540
+ fs.writeFileSync(ENV_FILE, minimalContent, { mode: 0o600 });
1541
+ // Keep gateway token secret intact
1542
+ ensureGatewayToken(existingEnv);
1543
+ }
1544
+
1545
+ // ── Route: Wizard (default for fresh install or wizard reconfigure) ───────
1546
+ log('Starting Limbo with setup wizard...');
1547
+ if (!alreadyHasEnv || (reconfig && hasProviderConfig)) {
1548
+ writeMinimalEnv();
1549
+ }
1550
+
1551
+ pullOrBuildImage('en');
1552
+ ensureVolumePermissions();
1553
+
1554
+ header('Starting Limbo...');
1555
+ log('Starting container...');
1556
+ const upResult = runDockerCompose(['up', '-d', '--remove-orphans'], { stdio: 'pipe' });
1557
+ if (upResult.status !== 0) {
1558
+ process.stderr.write(upResult.stderr || '');
1559
+ die('Container failed to start. Run `limbo logs` to investigate.');
1560
+ }
1561
+
1562
+ header('Waiting for setup wizard...');
1563
+ const healthy = waitForHealthy('en');
1564
+ if (!healthy) {
1565
+ warn('Container did not become healthy in time.');
1566
+ warn('Check logs with: limbo logs');
1567
+ } else {
1568
+ ok('Container is healthy.');
1452
1569
  }
1453
1570
 
1571
+ const wizardUrl = extractWizardUrl();
1572
+ if (wizardUrl) {
1573
+ printWizardUrl(wizardUrl);
1574
+ } else {
1575
+ // Fallback: container may have started without setup mode (e.g. config already inside volume)
1576
+ console.log(`
1577
+ ${c.yellow}Could not detect setup wizard URL.${c.reset}
1578
+ The container may already be configured.
1579
+
1580
+ Try: ${c.cyan}http://127.0.0.1:${PORT}${c.reset}
1581
+ Logs: ${c.dim}limbo logs${c.reset}
1582
+ `);
1583
+ }
1584
+ }
1585
+
1586
+ // Shared path for headless, CLI-prompt, and existing-config routes
1587
+ async function startContainerWithConfig(cfg, existingEnv, alreadyHasEnv) {
1454
1588
  const mergedEnv = parseEnvFile();
1455
1589
  if (!cfg.language) cfg.language = mergedEnv.CLI_LANGUAGE || 'en';
1456
1590
  if (!mergedEnv.CLI_LANGUAGE) {
@@ -1567,7 +1701,8 @@ ${c.bold}Commands:${c.reset}
1567
1701
  help Show this help
1568
1702
 
1569
1703
  ${c.bold}Flags:${c.reset}
1570
- --reconfigure Reconfigure auth and onboarding settings (use with start)
1704
+ --cli Use interactive CLI prompts instead of the web setup wizard
1705
+ --reconfigure Reconfigure settings (opens wizard, or CLI prompts with --cli)
1571
1706
  --hardened Enable egress proxy (restricts outbound to AI provider APIs only)
1572
1707
  --provider <name> Set provider for headless install (openai, anthropic, openrouter)
1573
1708
  --api-key <key> API key for headless install
@@ -0,0 +1,9 @@
1
+ # Dev override — mounts local code for testing without rebuild
2
+ # Usage: docker compose -f docker-compose.yml -f docker-compose.dev.yml up
3
+ services:
4
+ limbo:
5
+ volumes:
6
+ - ./setup-server:/app/setup-server:ro
7
+ - ./scripts/entrypoint.sh:/entrypoint.sh:ro
8
+ - ./migrations:/app/migrations:ro
9
+ - ./workspace:/app/workspace:ro
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limbo-ai",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Your personal AI memory agent — install and manage Limbo via npx",
5
5
  "type": "commonjs",
6
6
  "bin": {
File without changes