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.
Files changed (2) hide show
  1. package/cli.js +132 -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,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
- 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
+
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
- 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);
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
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limbo-ai",
3
- "version": "1.17.1",
3
+ "version": "1.18.1",
4
4
  "description": "Your personal AI memory agent — install and manage Limbo via npx",
5
5
  "type": "commonjs",
6
6
  "bin": {