superbrain-server 1.0.13 → 1.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superbrain-server",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "1-Line Auto-Installer and Server Execution wrapper for SuperBrain",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -34,6 +34,9 @@ instagrapi>=2.0.0
34
34
  opencv-python-headless>=4.9.0.80
35
35
  Pillow>=10.0.0
36
36
 
37
+ # ── Remote Access (Networking) ──────────────────────────────────────────────
38
+ pyngrok>=7.1.5
39
+
37
40
  # ── Terminal UI ───────────────────────────────────────────────────────────────
38
41
  rich>=13.0.0
39
- segno>=1.6.0
42
+ segno>=1.6.0
package/payload/start.py CHANGED
@@ -666,13 +666,16 @@ def setup_whisper():
666
666
  # ══════════════════════════════════════════════════════════════════════════════
667
667
  NGROK_ENABLED = BASE_DIR / "config" / "ngrok_enabled.txt"
668
668
  NGROK_TOKEN = BASE_DIR / "config" / "ngrok_token.txt"
669
+ LOCALTUNNEL_LOG = BASE_DIR / "config" / "localtunnel.log"
670
+ LOCALTUNNEL_LOG = BASE_DIR / "config" / "localtunnel.log"
669
671
 
670
672
  def setup_remote_access():
671
- h1("Step 6 of 7 — Remote Access (ngrok)")
673
+ h1("Step 6 of 7 — Remote Access (ngrok / localtunnel)")
672
674
 
673
675
  print(f"""
674
676
  The SuperBrain backend runs locally. Your phone needs to reach it over the internet.
675
677
  We recommend {BOLD}ngrok{RESET} for a secure tunnel.
678
+ If ngrok is not configured or fails, {BOLD}localtunnel{RESET} will be used as a fallback.
676
679
 
677
680
  Requires a free account from: {CYAN}https://dashboard.ngrok.com/signup{RESET}
678
681
  Get your Authtoken at: {CYAN}https://dashboard.ngrok.com/get-started/your-authtoken{RESET}
@@ -681,7 +684,7 @@ def setup_remote_access():
681
684
  choice = ask_yn("Enable ngrok on startup?", default=True)
682
685
  if not choice:
683
686
  NGROK_ENABLED.unlink(missing_ok=True)
684
- warn("Skipping ngrok. Local WiFi only.")
687
+ warn("Skipping ngrok. Will use localtunnel as fallback.")
685
688
  return
686
689
 
687
690
  ok("ngrok auto-start enabled")
@@ -733,7 +736,135 @@ def setup_token_and_db():
733
736
  # ══════════════════════════════════════════════════════════════════════════════
734
737
  # Launch Backend
735
738
  # ══════════════════════════════════════════════════════════════════════════════
736
- def _start_ngrok(port: int) -> str | None:
739
+ def _find_localtunnel_url_from_log() -> str | None:
740
+ try:
741
+ import re
742
+ if not LOCALTUNNEL_LOG.exists():
743
+ return None
744
+ text = LOCALTUNNEL_LOG.read_text(encoding="utf-8", errors="ignore")
745
+ m = re.search(r"https://[\w.-]+\.loca\.lt\b", text)
746
+ return m.group(0) if m else None
747
+ except Exception:
748
+ return None
749
+
750
+ def _stop_localtunnel_processes():
751
+ try:
752
+ if IS_WINDOWS:
753
+ script = (
754
+ "Get-CimInstance Win32_Process "
755
+ "| Where-Object { $_.CommandLine -match 'localtunnel|\\.loca\\.lt' } "
756
+ "| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
757
+ )
758
+ subprocess.run(["powershell", "-NoProfile", "-Command", script], check=False)
759
+ else:
760
+ subprocess.run(["pkill", "-f", "localtunnel"], check=False)
761
+ except Exception:
762
+ pass
763
+
764
+ def _start_localtunnel(port: int, timeout: int = 25) -> str | None:
765
+ import time
766
+ npx_exec = shutil.which("npx") or shutil.which("npx.cmd")
767
+ if not npx_exec:
768
+ warn("Node.js (npx) not found. Cannot start localtunnel.")
769
+ return None
770
+
771
+ _stop_localtunnel_processes()
772
+ time.sleep(0.8)
773
+
774
+ info("Starting localtunnel in background...")
775
+ try:
776
+ LOCALTUNNEL_LOG.parent.mkdir(parents=True, exist_ok=True)
777
+ LOCALTUNNEL_LOG.write_text("")
778
+ log_handle = open(LOCALTUNNEL_LOG, "a", encoding="utf-8", buffering=1)
779
+ kwargs = {
780
+ "start_new_session": True,
781
+ "stdout": log_handle,
782
+ "stderr": subprocess.STDOUT,
783
+ "text": True,
784
+ }
785
+ if IS_WINDOWS and npx_exec.lower().endswith(".cmd"):
786
+ cmd = ["cmd", "/c", npx_exec, "-y", "localtunnel", "--port", str(port)]
787
+ else:
788
+ cmd = [npx_exec, "-y", "localtunnel", "--port", str(port)]
789
+ subprocess.Popen(cmd, **kwargs)
790
+ except Exception as e:
791
+ warn(f"Could not start localtunnel: {e}")
792
+ return None
793
+
794
+ deadline = time.time() + timeout
795
+ while time.time() < deadline:
796
+ time.sleep(1)
797
+ url = _find_localtunnel_url_from_log()
798
+ if url:
799
+ return url
800
+ return None
801
+
802
+ def _find_localtunnel_url_from_log() -> str | None:
803
+ try:
804
+ import re
805
+ if not LOCALTUNNEL_LOG.exists():
806
+ return None
807
+ text = LOCALTUNNEL_LOG.read_text(encoding="utf-8", errors="ignore")
808
+ m = re.search(r"https://[\w.-]+\.loca\.lt\b", text)
809
+ return m.group(0) if m else None
810
+ except Exception:
811
+ return None
812
+
813
+ def _stop_localtunnel_processes():
814
+ try:
815
+ if IS_WINDOWS:
816
+ script = (
817
+ "Get-CimInstance Win32_Process "
818
+ "| Where-Object { $_.CommandLine -match 'localtunnel|\\.loca\\.lt' } "
819
+ "| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
820
+ )
821
+ subprocess.run(["powershell", "-NoProfile", "-Command", script], check=False)
822
+ else:
823
+ subprocess.run(["pkill", "-f", "localtunnel"], check=False)
824
+ except Exception:
825
+ pass
826
+
827
+ def _start_localtunnel(port: int, timeout: int = 25) -> str | None:
828
+ import time
829
+ npx_exec = shutil.which("npx") or shutil.which("npx.cmd")
830
+ if not npx_exec:
831
+ warn("Node.js (npx) not found. Cannot start localtunnel.")
832
+ return None
833
+
834
+ _stop_localtunnel_processes()
835
+ time.sleep(0.8)
836
+
837
+ info("Starting localtunnel in background...")
838
+ try:
839
+ LOCALTUNNEL_LOG.parent.mkdir(parents=True, exist_ok=True)
840
+ LOCALTUNNEL_LOG.write_text("")
841
+ log_handle = open(LOCALTUNNEL_LOG, "a", encoding="utf-8", buffering=1)
842
+ kwargs = {
843
+ "start_new_session": True,
844
+ "stdout": log_handle,
845
+ "stderr": subprocess.STDOUT,
846
+ "text": True,
847
+ }
848
+ if IS_WINDOWS and npx_exec.lower().endswith(".cmd"):
849
+ cmd = ["cmd", "/c", npx_exec, "-y", "localtunnel", "--port", str(port)]
850
+ else:
851
+ cmd = [npx_exec, "-y", "localtunnel", "--port", str(port)]
852
+ subprocess.Popen(cmd, **kwargs)
853
+ except Exception as e:
854
+ warn(f"Could not start localtunnel: {e}")
855
+ return None
856
+
857
+ deadline = time.time() + timeout
858
+ while time.time() < deadline:
859
+ time.sleep(1)
860
+ url = _find_localtunnel_url_from_log()
861
+ if url:
862
+ return url
863
+ return None
864
+
865
+ def _start_ngrok
866
+
867
+ (port: int) -> str | None:
737
868
  try:
738
869
  import pyngrok
739
870
  from pyngrok import ngrok
@@ -1115,8 +1246,10 @@ def launch_backend():
1115
1246
  token = TOKEN_FILE.read_text().strip() if TOKEN_FILE.exists() else "—"
1116
1247
  local_ip = _detect_local_ip()
1117
1248
 
1118
- # ngrok startup
1249
+ # tunnel startup
1119
1250
  public_url: str | None = None
1251
+ tunnel_type: str = ""
1252
+
1120
1253
  if NGROK_ENABLED.exists():
1121
1254
  token_txt = NGROK_TOKEN.read_text().strip() if NGROK_TOKEN.exists() else ""
1122
1255
  if not token_txt:
@@ -1125,19 +1258,30 @@ def launch_backend():
1125
1258
 
1126
1259
  info("Starting ngrok in background...")
1127
1260
  public_url = _start_ngrok(PORT)
1261
+ if public_url:
1262
+ tunnel_type = "ngrok"
1263
+ ok(f"ngrok active → {GREEN}{BOLD}{public_url}{RESET}")
1264
+ else:
1265
+ warn("ngrok failed. Falling back to localtunnel...")
1266
+
1267
+ if not public_url:
1268
+ public_url = _start_localtunnel(PORT)
1269
+ if public_url:
1270
+ tunnel_type = "localtunnel"
1271
+ ok(f"localtunnel active → {GREEN}{BOLD}{public_url}{RESET}")
1128
1272
 
1129
1273
  tunnel_line = ""
1130
1274
  tunnel_hint = ""
1131
1275
 
1132
1276
  if public_url:
1133
- tunnel_line = f" Public URL → {GREEN}{BOLD}{public_url}{RESET} {DIM}(ngrok){RESET}"
1277
+ tunnel_line = f" Public URL → {GREEN}{BOLD}{public_url}{RESET} {DIM}({tunnel_type}){RESET}"
1134
1278
  tunnel_hint = f" · public → {GREEN}{public_url}{RESET}"
1135
- ok(f"ngrok active → {GREEN}{BOLD}{public_url}{RESET}")
1136
1279
  elif NGROK_ENABLED.exists():
1137
- tunnel_line = f" Public URL → {YELLOW}(failed to start ngrok){RESET}"
1280
+ tunnel_line = f" Public URL → {YELLOW}(failed to start ngrok and localtunnel){RESET}"
1138
1281
  tunnel_hint = f" · public → run manually: {DIM}ngrok http {PORT}{RESET}"
1139
1282
  else:
1140
- tunnel_hint = f" · public enable ngrok via {DIM}python start.py --reset{RESET}"
1283
+ tunnel_line = f" Public URL → {YELLOW}(failed to start localtunnel){RESET}"
1284
+ tunnel_hint = f" · public → configure ngrok via {DIM}python start.py --reset{RESET} or ensure node/npx is installed"
1141
1285
 
1142
1286
  # ── Generate and display QR code ──────────────────────────────────────────
1143
1287
  qr_url = public_url if public_url else f"http://{local_ip}:{PORT}"
@@ -1195,6 +1339,7 @@ def launch_backend_status():
1195
1339
 
1196
1340
  # Fetch ngrok status via API
1197
1341
  url = "NOT_FOUND"
1342
+ tunnel_type = "tunnel"
1198
1343
  try:
1199
1344
  import urllib.request, json
1200
1345
  req = urllib.request.urlopen("http://127.0.0.1:4040/api/tunnels", timeout=2)
@@ -1202,12 +1347,20 @@ def launch_backend_status():
1202
1347
  for tunnel in data.get("tunnels", []):
1203
1348
  if tunnel.get("proto") == "https":
1204
1349
  url = tunnel.get("public_url")
1350
+ tunnel_type = "ngrok"
1205
1351
  break
1206
1352
  except Exception:
1207
1353
  pass
1208
1354
 
1209
1355
  if url == "NOT_FOUND":
1210
- warn("Could not find a running ngrok URL. Is the server running?")
1356
+ # Fallback to localtunnel log
1357
+ url_lt = _find_localtunnel_url_from_log()
1358
+ if url_lt:
1359
+ url = url_lt
1360
+ tunnel_type = "localtunnel"
1361
+
1362
+ if url == "NOT_FOUND":
1363
+ warn("Could not find a running ngrok or localtunnel URL. Is the server running?")
1211
1364
  nl()
1212
1365
  print(" Wait 5 seconds, or run 'superbrain-server' to start the server.")
1213
1366
  return
@@ -1219,11 +1372,10 @@ def launch_backend_status():
1219
1372
  network_url = f"http://{local_ip}:5000"
1220
1373
 
1221
1374
  nl()
1222
- print(f" Local URL \u2192 {CYAN}{local_url}{RESET}")
1223
- print(f" Network URL \u2192 {CYAN}{network_url}{RESET}")
1224
- print(f" Public URL \u2192 {CYAN}{url}{RESET} (localtunnel)")
1225
- print(f" API docs \u2192 {CYAN}{local_url}/docs{RESET}")
1226
- print(f" Access Token \u2192 {BOLD}{MAGENTA}{token}{RESET}")
1375
+ print(f" Local URL {CYAN}{local_url}{RESET}")
1376
+ print(f" Network URL {CYAN}{network_url}{RESET}")
1377
+ print(f" Public URL {CYAN}{url}{RESET} ({tunnel_type})")
1378
+ print(f" API docs {CYAN}{local_url}/docs{RESET}")
1227
1379
  nl()
1228
1380
 
1229
1381
  def main():