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 +167 -32
- package/docker-compose.dev.yml +9 -0
- package/package.json +1 -1
- package/setup-server/public/.gitkeep +0 -0
- package/setup-server/public/index.html +1888 -0
- package/setup-server/server.js +673 -0
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
|
|
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
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
--
|
|
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
|
File without changes
|