openclaw-navigator 5.2.2 → 5.3.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.mjs +116 -6
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * openclaw-navigator v5.2.0
4
+ * openclaw-navigator v5.3.1
5
5
  *
6
6
  * One-command bridge + tunnel for the Navigator browser.
7
7
  * Starts a local bridge, creates a Cloudflare tunnel automatically,
@@ -62,6 +62,10 @@ const validTokens = new Set();
62
62
  // OC Web UI reverse proxy target (configurable via --ui-port or env)
63
63
  let ocUIPort = parseInt(process.env.OPENCLAW_UI_PORT ?? "4000", 10);
64
64
 
65
+ // OC Web UI directory — the pre-installed Next.js app
66
+ const OC_UI_DIR = process.env.OPENCLAW_UI_DIR ?? join(homedir(), "openclaw-ui");
67
+ let uiProcess = null;
68
+
65
69
  // OC Gateway port — where sessions API + WebSocket live (separate from web UI)
66
70
  let ocGatewayPort = parseInt(process.env.OPENCLAW_GATEWAY_PORT ?? "18789", 10);
67
71
 
@@ -144,6 +148,77 @@ function writeJSON(filePath, data) {
144
148
  writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
145
149
  }
146
150
 
151
+ // ── OC Web UI lifecycle ───────────────────────────────────────────────────
152
+
153
+ /** Kill any process listening on a port (best-effort, macOS/Linux) */
154
+ async function killPort(port) {
155
+ const { execSync } = await import("node:child_process");
156
+ try {
157
+ // macOS: lsof -ti :PORT gives PIDs
158
+ const pids = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: "utf8" }).trim();
159
+ if (pids) {
160
+ for (const pid of pids.split("\n")) {
161
+ try {
162
+ execSync(`kill -9 ${pid.trim()} 2>/dev/null`);
163
+ info(` Killed process ${pid.trim()} on port ${port}`);
164
+ } catch {}
165
+ }
166
+ // Wait a beat for the port to free up
167
+ await new Promise((r) => setTimeout(r, 500));
168
+ }
169
+ } catch {
170
+ // No process on that port, or lsof not available — that's fine
171
+ }
172
+ }
173
+
174
+ /** Start the OC Web UI (Next.js) as a child process */
175
+ function startWebUI() {
176
+ if (!existsSync(join(OC_UI_DIR, "package.json"))) {
177
+ warn(`OC Web UI not found at ${OC_UI_DIR} — /ui/ proxy will return 502`);
178
+ info(` Set OPENCLAW_UI_DIR to the correct path, or install openclaw-ui there`);
179
+ return;
180
+ }
181
+
182
+ if (uiProcess) {
183
+ uiProcess.kill();
184
+ uiProcess = null;
185
+ }
186
+
187
+ uiProcess = spawn("npx", ["next", "start", "-p", String(ocUIPort)], {
188
+ cwd: OC_UI_DIR,
189
+ stdio: ["ignore", "pipe", "pipe"],
190
+ env: { ...process.env, PORT: String(ocUIPort), NODE_ENV: "production" },
191
+ });
192
+
193
+ uiProcess.stdout.on("data", (data) => {
194
+ const line = data.toString().trim();
195
+ if (line) {
196
+ info(` [ui] ${line}`);
197
+ }
198
+ });
199
+
200
+ uiProcess.stderr.on("data", (data) => {
201
+ const line = data.toString().trim();
202
+ if (line && !line.includes("ExperimentalWarning")) {
203
+ info(` [ui] ${line}`);
204
+ }
205
+ });
206
+
207
+ uiProcess.on("error", (err) => {
208
+ warn(`OC Web UI failed to start: ${err.message}`);
209
+ uiProcess = null;
210
+ });
211
+
212
+ uiProcess.on("exit", (code) => {
213
+ if (code !== null && code !== 0) {
214
+ warn(`OC Web UI exited with code ${code}`);
215
+ }
216
+ uiProcess = null;
217
+ });
218
+
219
+ ok(`OC Web UI starting on port ${ocUIPort} (PID ${uiProcess.pid}) from ${OC_UI_DIR}`);
220
+ }
221
+
147
222
  // Pairing code state
148
223
  let pairingCode = null;
149
224
  let pairingData = null;
@@ -891,11 +966,13 @@ function handleRequest(req, res) {
891
966
  return;
892
967
  }
893
968
 
894
- // ── Fallback: proxy unmatched paths → OC Web UI (for static assets) ──
895
- // The web UI at localhost:4000 may serve assets at /_next/*, /static/*, etc.
896
- // These don't start with /ui/ but still need to reach the web UI server.
969
+ // ── Fallback: proxy unmatched paths → OC Web UI ──────────────────────
970
+ // Catches /login, /_next/*, /static/*, /favicon.ico, etc.
971
+ // MUST apply the same cookie/redirect/forwarded-header treatment as /ui/*
972
+ // or login won't work (session cookies get lost through the tunnel).
897
973
  {
898
974
  const targetURL = `${path}${url.search}`;
975
+ const incomingHost = req.headers.host || "localhost";
899
976
 
900
977
  const proxyOpts = {
901
978
  hostname: "127.0.0.1",
@@ -905,14 +982,38 @@ function handleRequest(req, res) {
905
982
  headers: {
906
983
  ...req.headers,
907
984
  host: `127.0.0.1:${ocUIPort}`,
985
+ "x-forwarded-host": incomingHost,
986
+ "x-forwarded-proto": activeTunnelURL ? "https" : "http",
987
+ "x-forwarded-for": req.socket.remoteAddress || "127.0.0.1",
908
988
  },
909
989
  };
910
990
 
911
991
  const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
912
992
  const headers = { ...proxyRes.headers };
993
+
994
+ // CORS
913
995
  headers["access-control-allow-origin"] = "*";
914
996
  headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
915
- headers["access-control-allow-headers"] = "Content-Type, Authorization";
997
+ headers["access-control-allow-headers"] = "Content-Type, Authorization, Cookie";
998
+ headers["access-control-allow-credentials"] = "true";
999
+
1000
+ // Fix redirects — strip localhost prefix so browser stays on tunnel URL
1001
+ if (headers.location) {
1002
+ headers.location = headers.location
1003
+ .replace(`http://127.0.0.1:${ocUIPort}`, "")
1004
+ .replace(`http://localhost:${ocUIPort}`, "");
1005
+ }
1006
+
1007
+ // Fix cookies — remove domain restriction + secure flag for tunnel access
1008
+ if (headers["set-cookie"]) {
1009
+ const cookies = Array.isArray(headers["set-cookie"])
1010
+ ? headers["set-cookie"]
1011
+ : [headers["set-cookie"]];
1012
+ headers["set-cookie"] = cookies.map((c) =>
1013
+ c.replace(/;\s*domain=[^;]*/gi, "").replace(/;\s*secure/gi, ""),
1014
+ );
1015
+ }
1016
+
916
1017
  res.writeHead(proxyRes.statusCode ?? 502, headers);
917
1018
  proxyRes.pipe(res, { end: true });
918
1019
  });
@@ -1173,7 +1274,7 @@ module.exports = {
1173
1274
  });
1174
1275
 
1175
1276
  gwSocket.on("error", (err) => {
1176
- console.log(` ${DIM}WS proxy: gateway unreachable — ${err.message}${RESET}`);
1277
+ warn(`WS proxy: gateway on port ${ocGatewayPort} unreachable — ${err.message}`);
1177
1278
  socket.destroy();
1178
1279
  });
1179
1280
 
@@ -1204,6 +1305,11 @@ module.exports = {
1204
1305
 
1205
1306
  ok(`Bridge server running on ${bindHost}:${port}`);
1206
1307
 
1308
+ // ── Step 1.5: Start OC Web UI ──────────────────────────────────────
1309
+ // Kill anything on the UI port, then start the Next.js app as a child process.
1310
+ await killPort(ocUIPort);
1311
+ startWebUI();
1312
+
1207
1313
  // ── Step 2: Persistent identity ─────────────────────────────────────
1208
1314
  // Reuse the same pairing code + token across restarts so Navigator
1209
1315
  // doesn't need to re-pair. The code resolves to the NEW tunnel URL
@@ -1907,6 +2013,10 @@ module.exports = {
1907
2013
  // ── Graceful shutdown ─────────────────────────────────────────────────
1908
2014
  const shutdown = () => {
1909
2015
  console.log(`\n${DIM}Shutting down bridge...${RESET}`);
2016
+ if (uiProcess) {
2017
+ uiProcess.kill();
2018
+ uiProcess = null;
2019
+ }
1910
2020
  if (mcpProcess) {
1911
2021
  mcpProcess.kill();
1912
2022
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.2.2",
3
+ "version": "5.3.1",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",