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.
- package/cli.mjs +116 -6
- 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.
|
|
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
|
|
895
|
-
//
|
|
896
|
-
//
|
|
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
|
-
|
|
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
|
}
|