limbo-ai 1.16.0 → 1.17.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.
Files changed (2) hide show
  1. package/cli.js +197 -32
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -1001,6 +1001,34 @@ function ensureVolumePermissions() {
1001
1001
  ], { stdio: 'pipe' });
1002
1002
  }
1003
1003
 
1004
+ function installGlobalAlias() {
1005
+ // Create a `limbo` shell wrapper so users don't have to type `npx limbo-ai` every time.
1006
+ // Tries /usr/local/bin first (macOS, Linux with sudo), falls back to ~/.local/bin (no sudo).
1007
+ const wrapper = '#!/bin/sh\nexec npx limbo-ai "$@"\n';
1008
+ const candidates = [
1009
+ path.join(os.homedir(), '.local', 'bin', 'limbo'),
1010
+ '/usr/local/bin/limbo',
1011
+ ];
1012
+
1013
+ for (const target of candidates) {
1014
+ try {
1015
+ // Skip if already installed and current
1016
+ if (fs.existsSync(target)) {
1017
+ const existing = fs.readFileSync(target, 'utf8');
1018
+ if (existing.includes('limbo-ai')) return;
1019
+ }
1020
+ const dir = path.dirname(target);
1021
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1022
+ fs.writeFileSync(target, wrapper, { mode: 0o755 });
1023
+ log(`Installed ${c.bold}limbo${c.reset} command → ${target}`);
1024
+ return;
1025
+ } catch {
1026
+ // Permission denied — try next candidate
1027
+ }
1028
+ }
1029
+ // Silent failure — not critical, user can still use npx limbo-ai
1030
+ }
1031
+
1004
1032
  function isOomError(stderr) {
1005
1033
  return typeof stderr === 'string' && (
1006
1034
  stderr.includes('heap out of memory') ||
@@ -1352,16 +1380,97 @@ ${c.green}${c.bold}╚═══════════════════
1352
1380
  console.log(` "${t(cfg.language, 'nonTelegramPrompt', gatewayToken)}"`);
1353
1381
  }
1354
1382
 
1383
+ // ─── Docker auto-install ──────────────────────────────────────────────────────
1384
+
1385
+ function installDocker() {
1386
+ const platform = os.platform();
1387
+ if (platform === 'linux') {
1388
+ header('Installing Docker...');
1389
+ try {
1390
+ execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit' });
1391
+ ok('Docker installed.');
1392
+ } catch {
1393
+ die('Failed to install Docker. Install it manually: https://docs.docker.com/get-docker/');
1394
+ }
1395
+ } else if (platform === 'darwin') {
1396
+ die(`Docker is required but not installed.
1397
+
1398
+ Install Docker Desktop for Mac:
1399
+ ${c.cyan}https://docs.docker.com/desktop/install/mac-install/${c.reset}
1400
+
1401
+ Then run ${c.bold}npx limbo-ai${c.reset} again.`);
1402
+ } else {
1403
+ die('Docker is required but not installed. See https://docs.docker.com/get-docker/');
1404
+ }
1405
+ }
1406
+
1407
+ function extractWizardUrl(maxAttempts = 15) {
1408
+ for (let i = 1; i <= maxAttempts; i++) {
1409
+ try {
1410
+ const logs = runDockerCompose(['logs', '--no-log-prefix'], {
1411
+ stdio: 'pipe',
1412
+ encoding: 'utf8',
1413
+ });
1414
+ const output = logs.stdout || '';
1415
+ const match = output.match(/SETUP_URL=(https?:\/\/\S+)/);
1416
+ if (match) return match[1];
1417
+ } catch {}
1418
+ log(`Waiting for setup wizard URL... (${i}/${maxAttempts})`);
1419
+ sleep(2000);
1420
+ }
1421
+ return null;
1422
+ }
1423
+
1424
+ function printWizardUrl(url) {
1425
+ const displayUrl = url.replace('0.0.0.0', '127.0.0.1');
1426
+ console.log(`
1427
+ ${c.green}${c.bold}╔════════════════════════════════════════════════════════╗${c.reset}
1428
+ ${c.green}${c.bold}║ Setup wizard is ready! ║${c.reset}
1429
+ ${c.green}${c.bold}╚════════════════════════════════════════════════════════╝${c.reset}
1430
+
1431
+ Open this URL to complete setup:
1432
+
1433
+ ${c.cyan}${c.bold}${displayUrl}${c.reset}
1434
+
1435
+ The wizard will guide you through provider, API key, and model selection.
1436
+ Once complete, Limbo will restart and be ready to use.
1437
+
1438
+ ${c.dim}Logs: limbo logs | Stop: limbo stop${c.reset}
1439
+ `);
1440
+ // Auto-open on macOS
1441
+ if (os.platform() === 'darwin') {
1442
+ try { execSync(`open "${displayUrl}"`, { stdio: 'pipe' }); } catch {}
1443
+ }
1444
+ }
1445
+
1446
+ function writeMinimalEnv() {
1447
+ ensureComposeFile(false);
1448
+ const gatewayToken = ensureGatewayToken({});
1449
+ const content = `CLI_LANGUAGE=en\nLIMBO_PORT=${PORT}\n`;
1450
+ fs.writeFileSync(ENV_FILE, content, { mode: 0o600 });
1451
+ // Ensure gateway token secret exists for compose
1452
+ writeSecretFile('gateway_token', gatewayToken);
1453
+ return gatewayToken;
1454
+ }
1455
+
1355
1456
  // ─── Commands ────────────────────────────────────────────────────────────────
1356
1457
 
1357
1458
  async function cmdStart() {
1358
- if (!hasDocker()) die(t('en', 'dockerMissing'));
1459
+ // ── Auto-install Docker if missing ────────────────────────────────────────
1460
+ if (!hasDocker()) {
1461
+ installDocker();
1462
+ // Verify it worked
1463
+ if (!hasDocker()) die(t('en', 'dockerMissing'));
1464
+ }
1359
1465
 
1360
1466
  const hardened = process.argv.includes('--hardened');
1467
+ const cliMode = process.argv.includes('--cli');
1468
+ const reconfig = process.argv.includes('--reconfigure');
1361
1469
 
1362
- // ── Detect existing OpenClaw ──────────────────────────────────────────────
1470
+ // ── Detect existing OpenClaw / port selection ─────────────────────────────
1363
1471
  const existingEnv = parseEnvFile();
1364
1472
  const alreadyHasEnv = fs.existsSync(ENV_FILE);
1473
+ const hasProviderConfig = alreadyHasEnv && existingEnv.MODEL_PROVIDER;
1365
1474
 
1366
1475
  if (existingEnv.LIMBO_PORT) {
1367
1476
  const parsed = parseInt(existingEnv.LIMBO_PORT, 10);
@@ -1385,10 +1494,8 @@ async function cmdStart() {
1385
1494
  }
1386
1495
 
1387
1496
  ensureComposeFile(hardened);
1388
- let cfg;
1389
- let lang = existingEnv.CLI_LANGUAGE || 'en';
1390
1497
 
1391
- // ── Headless mode ──────────────────────────────────────────────────────────
1498
+ // ── Route: Headless (--provider flag) ─────────────────────────────────────
1392
1499
  const flagProvider = parseFlag('--provider');
1393
1500
  const flagApiKey = parseFlag('--api-key');
1394
1501
  const flagModel = parseFlag('--model');
@@ -1403,13 +1510,13 @@ async function cmdStart() {
1403
1510
  die(t(flagLang, 'headlessMissingApiKey'));
1404
1511
  }
1405
1512
 
1406
- lang = flagLang;
1513
+ const lang = flagLang;
1407
1514
  const providerFamily = deriveProviderFamily(flagProvider);
1408
1515
  const catalog = getModelCatalog(providerFamily, 'api-key');
1409
1516
  const modelName = flagModel || catalog.defaultModel;
1410
1517
 
1411
1518
  log(t(lang, 'headlessStarting'));
1412
- cfg = {
1519
+ const cfg = {
1413
1520
  language: lang,
1414
1521
  authMode: 'api-key',
1415
1522
  provider: catalog.provider,
@@ -1423,34 +1530,89 @@ async function cmdStart() {
1423
1530
  };
1424
1531
  writeEnv({ ...cfg, CLI_LANGUAGE: cfg.language }, existingEnv);
1425
1532
  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);
1533
+ return startContainerWithConfig(cfg, existingEnv, alreadyHasEnv);
1534
+ }
1535
+
1536
+ // ── Route: Existing config, no reconfigure ────────────────────────────────
1537
+ if (hasProviderConfig && !reconfig) {
1538
+ const lang = existingEnv.CLI_LANGUAGE || 'en';
1539
+ log(t(lang, 'foundExistingConfig'));
1540
+ log(t(lang, 'reconfigureHint'));
1541
+ ensureGatewayToken(existingEnv);
1542
+ const cfg = {
1543
+ language: lang,
1544
+ provider: existingEnv.MODEL_PROVIDER || 'anthropic',
1545
+ providerFamily: deriveProviderFamily(existingEnv.MODEL_PROVIDER),
1546
+ authMode: existingEnv.AUTH_MODE || 'api-key',
1547
+ modelName: existingEnv.MODEL_NAME || 'claude-opus-4-6',
1548
+ telegramEnabled: existingEnv.TELEGRAM_ENABLED || 'false',
1549
+ };
1550
+ return startContainerWithConfig(cfg, existingEnv, alreadyHasEnv);
1551
+ }
1552
+
1553
+ // ── Route: CLI prompts (--cli flag or --reconfigure --cli) ────────────────
1554
+ if (cliMode) {
1555
+ const lang = existingEnv.CLI_LANGUAGE || 'en';
1556
+ header(reconfig ? t(lang, 'reconfiguration') : t('en', 'configuration'));
1557
+ const cfg = await collectConfig(existingEnv);
1450
1558
  writeEnv({ ...cfg, CLI_LANGUAGE: cfg.language }, existingEnv);
1451
1559
  ok(t(cfg.language, 'envWritten'));
1560
+ return startContainerWithConfig(cfg, existingEnv, alreadyHasEnv);
1452
1561
  }
1453
1562
 
1563
+ // ── Route: Wizard reconfigure (--reconfigure, no --cli) ───────────────────
1564
+ if (reconfig && hasProviderConfig) {
1565
+ log('Resetting configuration for setup wizard...');
1566
+ // Remove provider config from .env so container enters setup mode
1567
+ const minimalContent = `CLI_LANGUAGE=${existingEnv.CLI_LANGUAGE || 'en'}\nLIMBO_PORT=${PORT}\n`;
1568
+ fs.writeFileSync(ENV_FILE, minimalContent, { mode: 0o600 });
1569
+ // Keep gateway token secret intact
1570
+ ensureGatewayToken(existingEnv);
1571
+ }
1572
+
1573
+ // ── Route: Wizard (default for fresh install or wizard reconfigure) ───────
1574
+ log('Starting Limbo with setup wizard...');
1575
+ if (!alreadyHasEnv || (reconfig && hasProviderConfig)) {
1576
+ writeMinimalEnv();
1577
+ }
1578
+
1579
+ pullOrBuildImage('en');
1580
+ ensureVolumePermissions();
1581
+
1582
+ header('Starting Limbo...');
1583
+ log('Starting container...');
1584
+ const upResult = runDockerCompose(['up', '-d', '--remove-orphans'], { stdio: 'pipe' });
1585
+ if (upResult.status !== 0) {
1586
+ process.stderr.write(upResult.stderr || '');
1587
+ die('Container failed to start. Run `limbo logs` to investigate.');
1588
+ }
1589
+
1590
+ header('Waiting for setup wizard...');
1591
+ const healthy = waitForHealthy('en');
1592
+ if (!healthy) {
1593
+ warn('Container did not become healthy in time.');
1594
+ warn('Check logs with: limbo logs');
1595
+ } else {
1596
+ ok('Container is healthy.');
1597
+ }
1598
+
1599
+ const wizardUrl = extractWizardUrl();
1600
+ if (wizardUrl) {
1601
+ printWizardUrl(wizardUrl);
1602
+ } else {
1603
+ // Fallback: container may have started without setup mode (e.g. config already inside volume)
1604
+ console.log(`
1605
+ ${c.yellow}Could not detect setup wizard URL.${c.reset}
1606
+ The container may already be configured.
1607
+
1608
+ Try: ${c.cyan}http://127.0.0.1:${PORT}${c.reset}
1609
+ Logs: ${c.dim}limbo logs${c.reset}
1610
+ `);
1611
+ }
1612
+ }
1613
+
1614
+ // Shared path for headless, CLI-prompt, and existing-config routes
1615
+ async function startContainerWithConfig(cfg, existingEnv, alreadyHasEnv) {
1454
1616
  const mergedEnv = parseEnvFile();
1455
1617
  if (!cfg.language) cfg.language = mergedEnv.CLI_LANGUAGE || 'en';
1456
1618
  if (!mergedEnv.CLI_LANGUAGE) {
@@ -1489,6 +1651,8 @@ async function cmdStart() {
1489
1651
 
1490
1652
  console.log(`\n ${c.yellow}⚠ ${t(cfg.language, 'securityNotice')}${c.reset}\n`);
1491
1653
 
1654
+ installGlobalAlias();
1655
+
1492
1656
  printSuccess({
1493
1657
  language: cfg.language,
1494
1658
  telegramEnabled: mergedEnv.TELEGRAM_ENABLED || cfg.telegramEnabled || 'false',
@@ -1567,7 +1731,8 @@ ${c.bold}Commands:${c.reset}
1567
1731
  help Show this help
1568
1732
 
1569
1733
  ${c.bold}Flags:${c.reset}
1570
- --reconfigure Reconfigure auth and onboarding settings (use with start)
1734
+ --cli Use interactive CLI prompts instead of the web setup wizard
1735
+ --reconfigure Reconfigure settings (opens wizard, or CLI prompts with --cli)
1571
1736
  --hardened Enable egress proxy (restricts outbound to AI provider APIs only)
1572
1737
  --provider <name> Set provider for headless install (openai, anthropic, openrouter)
1573
1738
  --api-key <key> API key for headless install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limbo-ai",
3
- "version": "1.16.0",
3
+ "version": "1.17.1",
4
4
  "description": "Your personal AI memory agent — install and manage Limbo via npx",
5
5
  "type": "commonjs",
6
6
  "bin": {