limbo-ai 1.17.0 → 1.18.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.
Files changed (2) hide show
  1. package/cli.js +162 -7
  2. 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,140 @@ 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', '--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
+
1110
+ function installGlobalAlias() {
1111
+ // Create a `limbo` shell wrapper so users don't have to type `npx limbo-ai` every time.
1112
+ // Tries /usr/local/bin first (macOS, Linux with sudo), falls back to ~/.local/bin (no sudo).
1113
+ const wrapper = '#!/bin/sh\nexec npx limbo-ai "$@"\n';
1114
+ const candidates = [
1115
+ path.join(os.homedir(), '.local', 'bin', 'limbo'),
1116
+ '/usr/local/bin/limbo',
1117
+ ];
1118
+
1119
+ for (const target of candidates) {
1120
+ try {
1121
+ // Skip if already installed and current
1122
+ if (fs.existsSync(target)) {
1123
+ const existing = fs.readFileSync(target, 'utf8');
1124
+ if (existing.includes('limbo-ai')) return;
1125
+ }
1126
+ const dir = path.dirname(target);
1127
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1128
+ fs.writeFileSync(target, wrapper, { mode: 0o755 });
1129
+ log(`Installed ${c.bold}limbo${c.reset} command → ${target}`);
1130
+ return;
1131
+ } catch {
1132
+ // Permission denied — try next candidate
1133
+ }
1134
+ }
1135
+ // Silent failure — not critical, user can still use npx limbo-ai
1136
+ }
1137
+
1004
1138
  function isOomError(stderr) {
1005
1139
  return typeof stderr === 'string' && (
1006
1140
  stderr.includes('heap out of memory') ||
@@ -1393,8 +1527,18 @@ function extractWizardUrl(maxAttempts = 15) {
1393
1527
  return null;
1394
1528
  }
1395
1529
 
1396
- function printWizardUrl(url) {
1397
- const displayUrl = url.replace('0.0.0.0', '127.0.0.1');
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
+
1398
1542
  console.log(`
1399
1543
  ${c.green}${c.bold}╔════════════════════════════════════════════════════════╗${c.reset}
1400
1544
  ${c.green}${c.bold}║ Setup wizard is ready! ║${c.reset}
@@ -1403,14 +1547,15 @@ ${c.green}${c.bold}╚═══════════════════
1403
1547
  Open this URL to complete setup:
1404
1548
 
1405
1549
  ${c.cyan}${c.bold}${displayUrl}${c.reset}
1406
-
1550
+ ${tunnel ? `
1551
+ ${c.green}🔒 Secured via Cloudflare (${tunnel.type === 'branded' ? tunnel.url.replace('https://', '') : 'HTTPS tunnel'})${c.reset}` : ''}
1407
1552
  The wizard will guide you through provider, API key, and model selection.
1408
1553
  Once complete, Limbo will restart and be ready to use.
1409
1554
 
1410
1555
  ${c.dim}Logs: limbo logs | Stop: limbo stop${c.reset}
1411
1556
  `);
1412
1557
  // Auto-open on macOS
1413
- if (os.platform() === 'darwin') {
1558
+ if (os.platform() === 'darwin' && !tunnel) {
1414
1559
  try { execSync(`open "${displayUrl}"`, { stdio: 'pipe' }); } catch {}
1415
1560
  }
1416
1561
  }
@@ -1472,6 +1617,7 @@ async function cmdStart() {
1472
1617
  const flagApiKey = parseFlag('--api-key');
1473
1618
  const flagModel = parseFlag('--model');
1474
1619
  const flagLang = parseFlag('--language') || 'en';
1620
+ const flagTunnelDomain = parseFlag('--tunnel-domain');
1475
1621
 
1476
1622
  if (flagProvider) {
1477
1623
  const validProviders = ['openai', 'anthropic', 'openrouter'];
@@ -1570,7 +1716,13 @@ async function cmdStart() {
1570
1716
 
1571
1717
  const wizardUrl = extractWizardUrl();
1572
1718
  if (wizardUrl) {
1573
- printWizardUrl(wizardUrl);
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);
1574
1726
  } else {
1575
1727
  // Fallback: container may have started without setup mode (e.g. config already inside volume)
1576
1728
  console.log(`
@@ -1623,6 +1775,8 @@ async function startContainerWithConfig(cfg, existingEnv, alreadyHasEnv) {
1623
1775
 
1624
1776
  console.log(`\n ${c.yellow}⚠ ${t(cfg.language, 'securityNotice')}${c.reset}\n`);
1625
1777
 
1778
+ installGlobalAlias();
1779
+
1626
1780
  printSuccess({
1627
1781
  language: cfg.language,
1628
1782
  telegramEnabled: mergedEnv.TELEGRAM_ENABLED || cfg.telegramEnabled || 'false',
@@ -1708,6 +1862,7 @@ ${c.bold}Flags:${c.reset}
1708
1862
  --api-key <key> API key for headless install
1709
1863
  --model <name> Model name (optional, uses provider default)
1710
1864
  --language <code> Language: en, es (default: en)
1865
+ --tunnel-domain <d> Admin: use branded subdomain for setup tunnel (e.g. limbo.tomasward.com)
1711
1866
 
1712
1867
  ${c.bold}Data directory:${c.reset} ${LIMBO_DIR}
1713
1868
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limbo-ai",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Your personal AI memory agent — install and manage Limbo via npx",
5
5
  "type": "commonjs",
6
6
  "bin": {