limbo-ai 1.17.1 → 1.18.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 +132 -7
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -152,7 +152,7 @@ function composeContent() {
|
|
|
152
152
|
- /tmp:size=100M,noexec,nosuid,nodev
|
|
153
153
|
- /home/limbo/.npm:size=50M,noexec,nosuid,nodev
|
|
154
154
|
ports:
|
|
155
|
-
- "127.0.0.1:${PORT}:${PORT}"
|
|
155
|
+
- "${isServerEnvironment() ? '0.0.0.0' : '127.0.0.1'}:${PORT}:${PORT}"
|
|
156
156
|
volumes:
|
|
157
157
|
- limbo-data:/data
|
|
158
158
|
- ${VAULT_DIR}:/data/vault
|
|
@@ -212,7 +212,7 @@ function composeContentHardened() {
|
|
|
212
212
|
- /tmp:size=100M,noexec,nosuid,nodev
|
|
213
213
|
- /home/limbo/.npm:size=50M,noexec,nosuid,nodev
|
|
214
214
|
ports:
|
|
215
|
-
- "127.0.0.1:${PORT}:${PORT}"
|
|
215
|
+
- "${isServerEnvironment() ? '0.0.0.0' : '127.0.0.1'}:${PORT}:${PORT}"
|
|
216
216
|
volumes:
|
|
217
217
|
- limbo-data:/data
|
|
218
218
|
- ${VAULT_DIR}:/data/vault
|
|
@@ -1001,6 +1001,112 @@ function ensureVolumePermissions() {
|
|
|
1001
1001
|
], { stdio: 'pipe' });
|
|
1002
1002
|
}
|
|
1003
1003
|
|
|
1004
|
+
// ─── Server detection & Cloudflare tunnel for remote wizard access ──────────
|
|
1005
|
+
|
|
1006
|
+
function isServerEnvironment() {
|
|
1007
|
+
return !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT ||
|
|
1008
|
+
(os.platform() === 'linux' && !process.env.DISPLAY));
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function hasCloudflared() {
|
|
1012
|
+
try { execSync('which cloudflared', { stdio: 'pipe' }); return true; } catch { return false; }
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function installCloudflared() {
|
|
1016
|
+
log('Installing cloudflared...');
|
|
1017
|
+
const platform = os.platform();
|
|
1018
|
+
try {
|
|
1019
|
+
if (platform === 'linux') {
|
|
1020
|
+
execSync('curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /tmp/cloudflared && chmod +x /tmp/cloudflared && sudo mv /tmp/cloudflared /usr/local/bin/cloudflared', { stdio: 'pipe' });
|
|
1021
|
+
} else if (platform === 'darwin') {
|
|
1022
|
+
execSync('brew install cloudflared', { stdio: 'pipe' });
|
|
1023
|
+
}
|
|
1024
|
+
return true;
|
|
1025
|
+
} catch {
|
|
1026
|
+
warn('Could not install cloudflared automatically.');
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function createSetupTunnel(port, tunnelDomain) {
|
|
1032
|
+
if (!hasCloudflared() && !installCloudflared()) return null;
|
|
1033
|
+
if (!hasCloudflared()) return null;
|
|
1034
|
+
|
|
1035
|
+
const tunnelId = crypto.randomBytes(4).toString('hex');
|
|
1036
|
+
|
|
1037
|
+
// Admin mode: branded subdomain (requires cloudflared login for the zone)
|
|
1038
|
+
if (tunnelDomain) {
|
|
1039
|
+
const tunnelName = `limbo-setup-${tunnelId}`;
|
|
1040
|
+
const subdomain = `setup-${tunnelId}.${tunnelDomain}`;
|
|
1041
|
+
try {
|
|
1042
|
+
execSync(`cloudflared tunnel create ${tunnelName}`, { stdio: 'pipe', encoding: 'utf8' });
|
|
1043
|
+
execSync(`cloudflared tunnel route dns ${tunnelName} ${subdomain}`, { stdio: 'pipe', encoding: 'utf8' });
|
|
1044
|
+
|
|
1045
|
+
const cfHome = path.join(os.homedir(), '.cloudflared');
|
|
1046
|
+
const tunnelInfoRaw = execSync(`cloudflared tunnel info ${tunnelName} 2>&1`, { encoding: 'utf8', stdio: 'pipe' });
|
|
1047
|
+
const tunnelUuid = tunnelInfoRaw.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/)?.[1];
|
|
1048
|
+
if (!tunnelUuid) throw new Error('Could not find tunnel UUID');
|
|
1049
|
+
|
|
1050
|
+
const credFile = path.join(cfHome, `${tunnelUuid}.json`);
|
|
1051
|
+
const configPath = path.join(LIMBO_DIR, 'cloudflared-setup.yml');
|
|
1052
|
+
fs.writeFileSync(configPath, [
|
|
1053
|
+
`tunnel: ${tunnelUuid}`,
|
|
1054
|
+
`credentials-file: ${credFile}`,
|
|
1055
|
+
'ingress:',
|
|
1056
|
+
` - hostname: ${subdomain}`,
|
|
1057
|
+
` service: http://localhost:${port}`,
|
|
1058
|
+
' - service: http_status:404',
|
|
1059
|
+
].join('\n'));
|
|
1060
|
+
|
|
1061
|
+
const tunnelProc = spawn('cloudflared', ['tunnel', '--config', configPath, 'run'], {
|
|
1062
|
+
detached: true, stdio: 'ignore',
|
|
1063
|
+
});
|
|
1064
|
+
tunnelProc.unref();
|
|
1065
|
+
sleep(5000);
|
|
1066
|
+
|
|
1067
|
+
return { type: 'branded', url: `https://${subdomain}`, tunnelName, tunnelUuid, configPath, pid: tunnelProc.pid };
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
warn(`Could not create branded tunnel: ${err.message}`);
|
|
1070
|
+
warn('Make sure you ran `cloudflared login` for this domain first.');
|
|
1071
|
+
// Fall through to quick tunnel
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Default: quick tunnel (zero config, works for everyone)
|
|
1076
|
+
try {
|
|
1077
|
+
const logFile = path.join(LIMBO_DIR, 'cloudflared-setup.log');
|
|
1078
|
+
const tunnelProc = spawn('cloudflared', ['tunnel', '--no-autoupdate', '--config', '/dev/null', '--url', `http://localhost:${port}`], {
|
|
1079
|
+
detached: true,
|
|
1080
|
+
stdio: ['ignore', fs.openSync(logFile, 'w'), fs.openSync(logFile, 'a')],
|
|
1081
|
+
});
|
|
1082
|
+
tunnelProc.unref();
|
|
1083
|
+
|
|
1084
|
+
for (let i = 0; i < 15; i++) {
|
|
1085
|
+
sleep(1000);
|
|
1086
|
+
try {
|
|
1087
|
+
const logs = fs.readFileSync(logFile, 'utf8');
|
|
1088
|
+
const match = logs.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
1089
|
+
if (match) return { type: 'quick', url: match[0], pid: tunnelProc.pid, logFile };
|
|
1090
|
+
} catch {}
|
|
1091
|
+
}
|
|
1092
|
+
warn('Could not start cloudflare tunnel.');
|
|
1093
|
+
return null;
|
|
1094
|
+
} catch {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function teardownSetupTunnel(tunnel) {
|
|
1100
|
+
if (!tunnel) return;
|
|
1101
|
+
try { process.kill(tunnel.pid); } catch {}
|
|
1102
|
+
|
|
1103
|
+
if (tunnel.type === 'branded') {
|
|
1104
|
+
try { execSync(`cloudflared tunnel delete -f ${tunnel.tunnelName}`, { stdio: 'pipe' }); } catch {}
|
|
1105
|
+
try { fs.unlinkSync(tunnel.configPath); } catch {}
|
|
1106
|
+
}
|
|
1107
|
+
if (tunnel.logFile) try { fs.unlinkSync(tunnel.logFile); } catch {}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1004
1110
|
function installGlobalAlias() {
|
|
1005
1111
|
// Create a `limbo` shell wrapper so users don't have to type `npx limbo-ai` every time.
|
|
1006
1112
|
// Tries /usr/local/bin first (macOS, Linux with sudo), falls back to ~/.local/bin (no sudo).
|
|
@@ -1421,8 +1527,18 @@ function extractWizardUrl(maxAttempts = 15) {
|
|
|
1421
1527
|
return null;
|
|
1422
1528
|
}
|
|
1423
1529
|
|
|
1424
|
-
function printWizardUrl(url) {
|
|
1425
|
-
|
|
1530
|
+
function printWizardUrl(url, tunnel) {
|
|
1531
|
+
// Extract token from the original URL
|
|
1532
|
+
const tokenMatch = url.match(/[?&]token=([^&\s]+)/);
|
|
1533
|
+
const token = tokenMatch ? tokenMatch[1] : '';
|
|
1534
|
+
|
|
1535
|
+
let displayUrl;
|
|
1536
|
+
if (tunnel) {
|
|
1537
|
+
displayUrl = `${tunnel.url}/?token=${token}`;
|
|
1538
|
+
} else {
|
|
1539
|
+
displayUrl = url.replace('0.0.0.0', '127.0.0.1');
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1426
1542
|
console.log(`
|
|
1427
1543
|
${c.green}${c.bold}╔════════════════════════════════════════════════════════╗${c.reset}
|
|
1428
1544
|
${c.green}${c.bold}║ Setup wizard is ready! ║${c.reset}
|
|
@@ -1431,14 +1547,15 @@ ${c.green}${c.bold}╚═══════════════════
|
|
|
1431
1547
|
Open this URL to complete setup:
|
|
1432
1548
|
|
|
1433
1549
|
${c.cyan}${c.bold}${displayUrl}${c.reset}
|
|
1434
|
-
|
|
1550
|
+
${tunnel ? `
|
|
1551
|
+
${c.green}🔒 Secured via Cloudflare (${tunnel.type === 'branded' ? tunnel.url.replace('https://', '') : 'HTTPS tunnel'})${c.reset}` : ''}
|
|
1435
1552
|
The wizard will guide you through provider, API key, and model selection.
|
|
1436
1553
|
Once complete, Limbo will restart and be ready to use.
|
|
1437
1554
|
|
|
1438
1555
|
${c.dim}Logs: limbo logs | Stop: limbo stop${c.reset}
|
|
1439
1556
|
`);
|
|
1440
1557
|
// Auto-open on macOS
|
|
1441
|
-
if (os.platform() === 'darwin') {
|
|
1558
|
+
if (os.platform() === 'darwin' && !tunnel) {
|
|
1442
1559
|
try { execSync(`open "${displayUrl}"`, { stdio: 'pipe' }); } catch {}
|
|
1443
1560
|
}
|
|
1444
1561
|
}
|
|
@@ -1500,6 +1617,7 @@ async function cmdStart() {
|
|
|
1500
1617
|
const flagApiKey = parseFlag('--api-key');
|
|
1501
1618
|
const flagModel = parseFlag('--model');
|
|
1502
1619
|
const flagLang = parseFlag('--language') || 'en';
|
|
1620
|
+
const flagTunnelDomain = parseFlag('--tunnel-domain');
|
|
1503
1621
|
|
|
1504
1622
|
if (flagProvider) {
|
|
1505
1623
|
const validProviders = ['openai', 'anthropic', 'openrouter'];
|
|
@@ -1598,7 +1716,13 @@ async function cmdStart() {
|
|
|
1598
1716
|
|
|
1599
1717
|
const wizardUrl = extractWizardUrl();
|
|
1600
1718
|
if (wizardUrl) {
|
|
1601
|
-
|
|
1719
|
+
// On servers, create a secure tunnel for remote access
|
|
1720
|
+
let tunnel = null;
|
|
1721
|
+
if (isServerEnvironment()) {
|
|
1722
|
+
log('Server environment detected — creating secure tunnel...');
|
|
1723
|
+
tunnel = createSetupTunnel(PORT, flagTunnelDomain);
|
|
1724
|
+
}
|
|
1725
|
+
printWizardUrl(wizardUrl, tunnel);
|
|
1602
1726
|
} else {
|
|
1603
1727
|
// Fallback: container may have started without setup mode (e.g. config already inside volume)
|
|
1604
1728
|
console.log(`
|
|
@@ -1738,6 +1862,7 @@ ${c.bold}Flags:${c.reset}
|
|
|
1738
1862
|
--api-key <key> API key for headless install
|
|
1739
1863
|
--model <name> Model name (optional, uses provider default)
|
|
1740
1864
|
--language <code> Language: en, es (default: en)
|
|
1865
|
+
--tunnel-domain <d> Admin: use branded subdomain for setup tunnel (e.g. limbo.tomasward.com)
|
|
1741
1866
|
|
|
1742
1867
|
${c.bold}Data directory:${c.reset} ${LIMBO_DIR}
|
|
1743
1868
|
`);
|