openclaw-navigator 4.5.0 → 4.7.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.mjs +214 -30
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * openclaw-navigator v4.0.0
4
+ * openclaw-navigator v4.1.0
5
5
  *
6
6
  * One-command bridge + tunnel for the Navigator browser.
7
7
  * Starts a local bridge, creates a Cloudflare tunnel automatically,
8
8
  * and gives you a 6-digit pairing code. Works on any OS.
9
+ * Auto-installs, builds, and starts the OC Web UI on first run.
9
10
  *
10
11
  * Usage:
11
12
  * npx openclaw-navigator Auto-tunnel (default)
@@ -131,6 +132,146 @@ function writeJSON(filePath, data) {
131
132
  writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
132
133
  }
133
134
 
135
+ // ── OC Web UI lifecycle ──────────────────────────────────────────────────────
136
+
137
+ const UI_DIR = join(homedir(), ".openclaw", "ui");
138
+ const UI_REPO = "https://github.com/sandman66666/openclaw-ui.git";
139
+ let uiProcess = null;
140
+
141
+ async function isUIInstalled() {
142
+ return existsSync(join(UI_DIR, "package.json")) && existsSync(join(UI_DIR, ".next"));
143
+ }
144
+
145
+ async function setupUI() {
146
+ const { execSync } = await import("node:child_process");
147
+
148
+ if (existsSync(join(UI_DIR, "package.json"))) {
149
+ info(" UI directory exists, reinstalling...");
150
+ return await buildUI();
151
+ }
152
+
153
+ heading("Setting up OC Web UI (first time)");
154
+ info(" Cloning from GitHub...");
155
+
156
+ mkdirSync(join(homedir(), ".openclaw"), { recursive: true });
157
+
158
+ try {
159
+ execSync(`git clone --depth 1 ${UI_REPO} "${UI_DIR}"`, {
160
+ stdio: ["ignore", "pipe", "pipe"],
161
+ timeout: 60000,
162
+ });
163
+ ok("Repository cloned");
164
+ } catch (err) {
165
+ fail(`Failed to clone UI repo: ${err.message}`);
166
+ return false;
167
+ }
168
+
169
+ return await buildUI();
170
+ }
171
+
172
+ async function buildUI() {
173
+ const { execSync } = await import("node:child_process");
174
+
175
+ process.stdout.write(` ${DIM}Installing dependencies (this may take a minute)...${RESET}`);
176
+ try {
177
+ execSync("npm install --production=false", {
178
+ cwd: UI_DIR,
179
+ stdio: ["ignore", "pipe", "pipe"],
180
+ timeout: 120000,
181
+ env: { ...process.env, NODE_ENV: "development" },
182
+ });
183
+ process.stdout.write(`\r${" ".repeat(70)}\r`);
184
+ ok("Dependencies installed");
185
+ } catch (err) {
186
+ process.stdout.write(`\r${" ".repeat(70)}\r`);
187
+ fail(`npm install failed: ${err.stderr?.toString()?.slice(-200) || err.message}`);
188
+ return false;
189
+ }
190
+
191
+ process.stdout.write(` ${DIM}Building web UI...${RESET}`);
192
+ try {
193
+ execSync("npx next build", {
194
+ cwd: UI_DIR,
195
+ stdio: ["ignore", "pipe", "pipe"],
196
+ timeout: 180000,
197
+ });
198
+ process.stdout.write(`\r${" ".repeat(70)}\r`);
199
+ ok("Web UI built successfully");
200
+ return true;
201
+ } catch (err) {
202
+ process.stdout.write(`\r${" ".repeat(70)}\r`);
203
+ fail(`Build failed: ${err.stderr?.toString()?.slice(-200) || err.message}`);
204
+ return false;
205
+ }
206
+ }
207
+
208
+ async function updateUI() {
209
+ const { execSync } = await import("node:child_process");
210
+
211
+ if (!existsSync(join(UI_DIR, ".git"))) {
212
+ warn("UI not installed yet — running full setup instead");
213
+ return await setupUI();
214
+ }
215
+
216
+ heading("Updating OC Web UI");
217
+
218
+ try {
219
+ const result = execSync("git pull --rebase origin main", {
220
+ cwd: UI_DIR,
221
+ encoding: "utf8",
222
+ timeout: 30000,
223
+ });
224
+
225
+ if (result.includes("Already up to date")) {
226
+ ok("Already up to date");
227
+ return true;
228
+ }
229
+
230
+ ok("Pulled latest changes");
231
+ return await buildUI();
232
+ } catch (err) {
233
+ fail(`Update failed: ${err.message}`);
234
+ return false;
235
+ }
236
+ }
237
+
238
+ function startUIServer(port) {
239
+ if (uiProcess) {
240
+ uiProcess.kill();
241
+ uiProcess = null;
242
+ }
243
+
244
+ uiProcess = spawn("npx", ["next", "start", "-p", String(port)], {
245
+ cwd: UI_DIR,
246
+ stdio: ["ignore", "pipe", "pipe"],
247
+ env: { ...process.env, PORT: String(port), NODE_ENV: "production" },
248
+ });
249
+
250
+ uiProcess.on("error", (err) => {
251
+ warn(`OC Web UI failed to start: ${err.message}`);
252
+ uiProcess = null;
253
+ });
254
+
255
+ uiProcess.on("exit", (code) => {
256
+ if (code !== null && code !== 0) {
257
+ warn(`OC Web UI exited with code ${code}`);
258
+ }
259
+ uiProcess = null;
260
+ });
261
+
262
+ return new Promise((resolve) => {
263
+ const timer = setTimeout(() => {
264
+ ok(`OC Web UI starting on port ${port} (PID ${uiProcess?.pid})`);
265
+ resolve(true);
266
+ }, 1500);
267
+
268
+ uiProcess.on("exit", () => {
269
+ clearTimeout(timer);
270
+ resolve(false);
271
+ });
272
+ });
273
+ }
274
+
134
275
  // Pairing code state
135
276
  let pairingCode = null;
136
277
  let pairingData = null;
@@ -265,6 +406,13 @@ function sendJSON(res, status, body) {
265
406
  res.end(JSON.stringify(body));
266
407
  }
267
408
 
409
+ function validateBridgeAuth(req) {
410
+ const authHeader = req.headers["authorization"];
411
+ if (!authHeader) return false;
412
+ const token = authHeader.replace(/^Bearer\s+/i, "");
413
+ return validTokens.has(token);
414
+ }
415
+
268
416
  function handleRequest(req, res) {
269
417
  // CORS preflight
270
418
  if (req.method === "OPTIONS") {
@@ -314,6 +462,10 @@ function handleRequest(req, res) {
314
462
 
315
463
  // ── GET /navigator/commands ──
316
464
  if (req.method === "GET" && path === "/navigator/commands") {
465
+ if (!validateBridgeAuth(req)) {
466
+ sendJSON(res, 401, { ok: false, error: "unauthorized", hint: "Include Authorization: Bearer <token> header" });
467
+ return;
468
+ }
317
469
  if (!bridgeState.connected) {
318
470
  bridgeState.connected = true;
319
471
  bridgeState.connectedAt = Date.now();
@@ -329,6 +481,10 @@ function handleRequest(req, res) {
329
481
 
330
482
  // ── POST /navigator/events ──
331
483
  if (req.method === "POST" && path === "/navigator/events") {
484
+ if (!validateBridgeAuth(req)) {
485
+ sendJSON(res, 401, { ok: false, error: "unauthorized", hint: "Include Authorization: Bearer <token> header" });
486
+ return;
487
+ }
332
488
  readBody(req)
333
489
  .then((bodyStr) => {
334
490
  try {
@@ -412,6 +568,10 @@ function handleRequest(req, res) {
412
568
 
413
569
  // ── POST /navigator/command ──
414
570
  if (req.method === "POST" && path === "/navigator/command") {
571
+ if (!validateBridgeAuth(req)) {
572
+ sendJSON(res, 401, { ok: false, error: "unauthorized", hint: "Include Authorization: Bearer <token> header" });
573
+ return;
574
+ }
415
575
  readBody(req)
416
576
  .then((bodyStr) => {
417
577
  try {
@@ -704,10 +864,9 @@ function handleRequest(req, res) {
704
864
  }
705
865
 
706
866
  // ── Reverse proxy: /ui/* → OC Web UI (localhost:ocUIPort) ──────────────
707
- // Strips /ui prefix so /ui/dashboard localhost:4000/dashboard
867
+ // Keep /ui prefix Next.js basePath: "/ui" expects it
708
868
  if (path === "/ui" || path.startsWith("/ui/")) {
709
- const targetPath = path === "/ui" ? "/" : path.slice(3); // strip "/ui"
710
- const targetURL = `${targetPath}${url.search}`;
869
+ const targetURL = `${path}${url.search}`;
711
870
 
712
871
  const proxyOpts = {
713
872
  hostname: "127.0.0.1",
@@ -782,32 +941,16 @@ function handleRequest(req, res) {
782
941
  return;
783
942
  }
784
943
 
785
- // ── Root → proxy to OC Web UI if available, else show bridge status ──
944
+ // ── Root → redirect to /ui/ (Next.js basePath) or show bridge status ──
786
945
  if (req.method === "GET" && path === "") {
787
946
  // Quick probe to check if OC UI is running
788
947
  const probe = httpRequest(
789
- { hostname: "127.0.0.1", port: ocUIPort, path: "/", method: "HEAD", timeout: 1000 },
948
+ { hostname: "127.0.0.1", port: ocUIPort, path: "/ui/", method: "HEAD", timeout: 1000 },
790
949
  (probeRes) => {
791
950
  probeRes.resume(); // drain
792
- // OC UI is running — proxy root to it
793
- const rootProxyOpts = {
794
- hostname: "127.0.0.1",
795
- port: ocUIPort,
796
- path: `/${url.search}`,
797
- method: req.method,
798
- headers: { ...req.headers, host: `127.0.0.1:${ocUIPort}` },
799
- };
800
- const rootProxy = httpRequest(rootProxyOpts, (rootRes) => {
801
- const headers = { ...rootRes.headers };
802
- headers["access-control-allow-origin"] = "*";
803
- res.writeHead(rootRes.statusCode ?? 502, headers);
804
- rootRes.pipe(res, { end: true });
805
- });
806
- rootProxy.on("error", () => {
807
- res.writeHead(302, { Location: "/ui/" });
808
- res.end();
809
- });
810
- rootProxy.end();
951
+ // OC UI is running — redirect to /ui/
952
+ res.writeHead(302, { Location: "/ui/" });
953
+ res.end();
811
954
  },
812
955
  );
813
956
  probe.on("error", () => {
@@ -815,8 +958,8 @@ function handleRequest(req, res) {
815
958
  sendJSON(res, 200, {
816
959
  ok: true,
817
960
  service: "openclaw-navigator-bridge",
818
- version: "4.4.0",
819
- ui: { available: false, port: ocUIPort, hint: "Start the OC gateway to enable /ui/" },
961
+ version: "4.6.0",
962
+ ui: { available: false, port: ocUIPort, hint: "Start the OC Web UI to enable /ui/" },
820
963
  gateway: { port: ocGatewayPort },
821
964
  navigator: { connected: bridgeState.connected },
822
965
  });
@@ -826,7 +969,7 @@ function handleRequest(req, res) {
826
969
  sendJSON(res, 200, {
827
970
  ok: true,
828
971
  service: "openclaw-navigator-bridge",
829
- version: "4.4.0",
972
+ version: "4.6.0",
830
973
  ui: { available: false, port: ocUIPort, hint: "OC UI timed out" },
831
974
  });
832
975
  });
@@ -931,6 +1074,9 @@ async function main() {
931
1074
  let tunnelToken = null; // For named tunnels (Cloudflare)
932
1075
  let tunnelHostname = null; // For named tunnels (stable URL)
933
1076
  let freshIdentity = false; // --new-code: force new pairing code
1077
+ let setupUIFlag = false;
1078
+ let updateUIFlag = false;
1079
+ let noUIFlag = false;
934
1080
 
935
1081
  for (let i = 0; i < args.length; i++) {
936
1082
  if (args[i] === "--port" && args[i + 1]) {
@@ -963,6 +1109,15 @@ async function main() {
963
1109
  if (args[i] === "--new-code") {
964
1110
  freshIdentity = true;
965
1111
  }
1112
+ if (args[i] === "--setup-ui") {
1113
+ setupUIFlag = true;
1114
+ }
1115
+ if (args[i] === "--update-ui") {
1116
+ updateUIFlag = true;
1117
+ }
1118
+ if (args[i] === "--no-ui") {
1119
+ noUIFlag = true;
1120
+ }
966
1121
  if (args[i] === "--help" || args[i] === "-h") {
967
1122
  console.log(`
968
1123
  ${BOLD}openclaw-navigator${RESET} — One-command bridge + tunnel for Navigator
@@ -981,6 +1136,9 @@ ${BOLD}Options:${RESET}
981
1136
  --no-tunnel Skip auto-tunnel, use SSH or LAN instead
982
1137
  --bind <host> Bind address (default: 127.0.0.1)
983
1138
  --new-code Force a new pairing code (discard saved identity)
1139
+ --setup-ui Force (re)install + build OC Web UI
1140
+ --update-ui Pull latest UI changes and rebuild
1141
+ --no-ui Don't auto-start the web UI
984
1142
  --help Show this help
985
1143
 
986
1144
  ${BOLD}Stability (recommended for production):${RESET}
@@ -1025,7 +1183,7 @@ module.exports = {
1025
1183
  apps: [{
1026
1184
  name: "openclaw-navigator",
1027
1185
  script: "${npxPath}",
1028
- args: "openclaw-navigator@latest --mcp --port ${port}",
1186
+ args: "openclaw-navigator@latest --mcp --no-ui --port ${port}",
1029
1187
  cwd: "${homedir()}",
1030
1188
  autorestart: true,
1031
1189
  max_restarts: 50,
@@ -1279,6 +1437,28 @@ module.exports = {
1279
1437
  console.log("");
1280
1438
  }
1281
1439
 
1440
+ // ── OC Web UI: auto-setup + start ─────────────────────────────────────
1441
+ if (!noUIFlag) {
1442
+ if (setupUIFlag) {
1443
+ await setupUI();
1444
+ } else if (updateUIFlag) {
1445
+ await updateUI();
1446
+ }
1447
+
1448
+ if (await isUIInstalled()) {
1449
+ await startUIServer(ocUIPort);
1450
+ } else if (!setupUIFlag && !noUIFlag) {
1451
+ heading("OC Web UI not found — setting up automatically");
1452
+ const setupOk = await setupUI();
1453
+ if (setupOk) {
1454
+ await startUIServer(ocUIPort);
1455
+ } else {
1456
+ warn("Web UI setup failed — you can retry with: npx openclaw-navigator --setup-ui");
1457
+ warn("The bridge will still work, but /ui/* won't serve the dashboard");
1458
+ }
1459
+ }
1460
+ }
1461
+
1282
1462
  // ── Step 4: Register initial pairing code with relay ────────────────
1283
1463
  if (tunnelURL && !tunnelToken) {
1284
1464
  // Already registered inside startOrReconnectTunnel()
@@ -1340,7 +1520,7 @@ module.exports = {
1340
1520
  };
1341
1521
 
1342
1522
  await Promise.all([
1343
- checkPort("OC Web UI", ocUIPort, "/"),
1523
+ checkPort("OC Web UI", ocUIPort, "/ui/"),
1344
1524
  checkPort("OC Gateway", ocGatewayPort, "/health"),
1345
1525
  ]);
1346
1526
 
@@ -1763,6 +1943,10 @@ module.exports = {
1763
1943
  // ── Graceful shutdown ─────────────────────────────────────────────────
1764
1944
  const shutdown = () => {
1765
1945
  console.log(`\n${DIM}Shutting down bridge...${RESET}`);
1946
+ if (uiProcess) {
1947
+ uiProcess.kill();
1948
+ uiProcess = null;
1949
+ }
1766
1950
  if (mcpProcess) {
1767
1951
  mcpProcess.kill();
1768
1952
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",