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.
- package/cli.js +197 -32
- 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
|
|
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
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
--
|
|
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
|