openclaw-navigator 4.3.8 → 4.4.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 +322 -22
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -17,6 +17,7 @@ import { spawn } from "node:child_process";
17
17
  import { randomUUID } from "node:crypto";
18
18
  import { existsSync } from "node:fs";
19
19
  import { createServer, request as httpRequest } from "node:http";
20
+ import { connect as netConnect } from "node:net";
20
21
  import { networkInterfaces, hostname, userInfo } from "node:os";
21
22
  import { dirname, join } from "node:path";
22
23
  import { fileURLToPath } from "node:url";
@@ -58,6 +59,12 @@ const validTokens = new Set();
58
59
  // OC Web UI reverse proxy target (configurable via --ui-port or env)
59
60
  let ocUIPort = parseInt(process.env.OPENCLAW_UI_PORT ?? "4000", 10);
60
61
 
62
+ // OC Gateway port — where sessions API + WebSocket live (separate from web UI)
63
+ let ocGatewayPort = parseInt(process.env.OPENCLAW_GATEWAY_PORT ?? "18789", 10);
64
+
65
+ // Bridge port — set in main(), exposed for status endpoint
66
+ let bridgePort = 18790;
67
+
61
68
  // Tunnel URL — set once the tunnel is active, exposed via /navigator/status
62
69
  let activeTunnelURL = null;
63
70
 
@@ -220,10 +227,24 @@ function handleRequest(req, res) {
220
227
  pendingCommandCount: pendingCommands.length,
221
228
  recentEventCount: recentEvents.length,
222
229
  },
223
- tunnel: activeTunnelURL ? {
224
- url: activeTunnelURL,
225
- uiURL: `${activeTunnelURL}/ui/`,
226
- } : null,
230
+ ports: {
231
+ bridge: bridgePort,
232
+ ui: ocUIPort,
233
+ gateway: ocGatewayPort,
234
+ },
235
+ routing: {
236
+ "/ui/*": `localhost:${ocUIPort}`,
237
+ "/api/*": `localhost:${ocGatewayPort}`,
238
+ "/ws": `localhost:${ocGatewayPort}`,
239
+ },
240
+ tunnel: activeTunnelURL
241
+ ? {
242
+ url: activeTunnelURL,
243
+ uiURL: `${activeTunnelURL}/ui/`,
244
+ apiURL: `${activeTunnelURL}/api/`,
245
+ wsURL: `${activeTunnelURL.replace("https://", "wss://")}/ws`,
246
+ }
247
+ : null,
227
248
  });
228
249
  return;
229
250
  }
@@ -454,7 +475,7 @@ function handleRequest(req, res) {
454
475
 
455
476
  // ── Health check ──
456
477
  if (req.method === "GET" && path === "/health") {
457
- sendJSON(res, 200, { ok: true, service: "openclaw-navigator-bridge", version: "4.3.4" });
478
+ sendJSON(res, 200, { ok: true, service: "openclaw-navigator-bridge", version: "4.4.0" });
458
479
  return;
459
480
  }
460
481
 
@@ -490,7 +511,7 @@ function handleRequest(req, res) {
490
511
  ok: false,
491
512
  error: `OC Web UI not reachable on port ${ocUIPort}`,
492
513
  detail: err.message,
493
- hint: "Make sure the OC gateway is running (openclaw gateway start)",
514
+ hint: `Not found — Reverse proxy rule for /ui/* → localhost:${ocUIPort} is working, but the web UI is not running. Start it with: openclaw gateway start`,
494
515
  });
495
516
  });
496
517
 
@@ -499,16 +520,70 @@ function handleRequest(req, res) {
499
520
  return;
500
521
  }
501
522
 
502
- // ── Root redirect to /ui/ if OC Web UI is available ──
523
+ // ── Reverse proxy: /api/* OC Gateway (localhost:ocGatewayPort) ─────
524
+ // Keeps /api/ prefix intact so /api/sessions/send → localhost:18789/api/sessions/send
525
+ if (path.startsWith("/api/")) {
526
+ const targetURL = `${path}${url.search}`;
527
+
528
+ const proxyOpts = {
529
+ hostname: "127.0.0.1",
530
+ port: ocGatewayPort,
531
+ path: targetURL,
532
+ method: req.method,
533
+ headers: {
534
+ ...req.headers,
535
+ host: `127.0.0.1:${ocGatewayPort}`,
536
+ },
537
+ };
538
+
539
+ const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
540
+ const headers = { ...proxyRes.headers };
541
+ headers["access-control-allow-origin"] = "*";
542
+ headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
543
+ headers["access-control-allow-headers"] = "Content-Type, Authorization";
544
+ res.writeHead(proxyRes.statusCode ?? 502, headers);
545
+ proxyRes.pipe(res, { end: true });
546
+ });
547
+
548
+ proxyReq.on("error", (err) => {
549
+ sendJSON(res, 502, {
550
+ ok: false,
551
+ error: `OC Gateway not reachable on port ${ocGatewayPort}`,
552
+ detail: err.message,
553
+ hint: "Make sure the OC gateway is running on port " + ocGatewayPort + " (openclaw gateway start)",
554
+ });
555
+ });
556
+
557
+ req.pipe(proxyReq, { end: true });
558
+ return;
559
+ }
560
+
561
+ // ── Root → proxy to OC Web UI if available, else show bridge status ──
503
562
  if (req.method === "GET" && path === "") {
504
563
  // Quick probe to check if OC UI is running
505
564
  const probe = httpRequest(
506
565
  { hostname: "127.0.0.1", port: ocUIPort, path: "/", method: "HEAD", timeout: 1000 },
507
566
  (probeRes) => {
508
567
  probeRes.resume(); // drain
509
- // OC UI is running — redirect to it
510
- res.writeHead(302, { Location: "/ui/" });
511
- res.end();
568
+ // OC UI is running — proxy root to it
569
+ const rootProxyOpts = {
570
+ hostname: "127.0.0.1",
571
+ port: ocUIPort,
572
+ path: `/${url.search}`,
573
+ method: req.method,
574
+ headers: { ...req.headers, host: `127.0.0.1:${ocUIPort}` },
575
+ };
576
+ const rootProxy = httpRequest(rootProxyOpts, (rootRes) => {
577
+ const headers = { ...rootRes.headers };
578
+ headers["access-control-allow-origin"] = "*";
579
+ res.writeHead(rootRes.statusCode ?? 502, headers);
580
+ rootRes.pipe(res, { end: true });
581
+ });
582
+ rootProxy.on("error", () => {
583
+ res.writeHead(302, { Location: "/ui/" });
584
+ res.end();
585
+ });
586
+ rootProxy.end();
512
587
  },
513
588
  );
514
589
  probe.on("error", () => {
@@ -516,8 +591,9 @@ function handleRequest(req, res) {
516
591
  sendJSON(res, 200, {
517
592
  ok: true,
518
593
  service: "openclaw-navigator-bridge",
519
- version: "4.3.4",
594
+ version: "4.4.0",
520
595
  ui: { available: false, port: ocUIPort, hint: "Start the OC gateway to enable /ui/" },
596
+ gateway: { port: ocGatewayPort },
521
597
  navigator: { connected: bridgeState.connected },
522
598
  });
523
599
  });
@@ -526,7 +602,7 @@ function handleRequest(req, res) {
526
602
  sendJSON(res, 200, {
527
603
  ok: true,
528
604
  service: "openclaw-navigator-bridge",
529
- version: "4.3.4",
605
+ version: "4.4.0",
530
606
  ui: { available: false, port: ocUIPort, hint: "OC UI timed out" },
531
607
  });
532
608
  });
@@ -534,7 +610,39 @@ function handleRequest(req, res) {
534
610
  return;
535
611
  }
536
612
 
537
- sendJSON(res, 404, { ok: false, error: "Not found" });
613
+ // ── Fallback: proxy unmatched paths OC Web UI (for static assets) ──
614
+ // The web UI at localhost:4000 may serve assets at /_next/*, /static/*, etc.
615
+ // These don't start with /ui/ but still need to reach the web UI server.
616
+ {
617
+ const targetURL = `${path}${url.search}`;
618
+
619
+ const proxyOpts = {
620
+ hostname: "127.0.0.1",
621
+ port: ocUIPort,
622
+ path: targetURL,
623
+ method: req.method,
624
+ headers: {
625
+ ...req.headers,
626
+ host: `127.0.0.1:${ocUIPort}`,
627
+ },
628
+ };
629
+
630
+ const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
631
+ const headers = { ...proxyRes.headers };
632
+ headers["access-control-allow-origin"] = "*";
633
+ headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
634
+ headers["access-control-allow-headers"] = "Content-Type, Authorization";
635
+ res.writeHead(proxyRes.statusCode ?? 502, headers);
636
+ proxyRes.pipe(res, { end: true });
637
+ });
638
+
639
+ proxyReq.on("error", () => {
640
+ sendJSON(res, 404, { ok: false, error: "Not found" });
641
+ });
642
+
643
+ req.pipe(proxyReq, { end: true });
644
+ return;
645
+ }
538
646
  }
539
647
 
540
648
  // ── Relay registration ────────────────────────────────────────────────────
@@ -612,6 +720,9 @@ async function main() {
612
720
  if (args[i] === "--ui-port" && args[i + 1]) {
613
721
  ocUIPort = parseInt(args[i + 1], 10);
614
722
  }
723
+ if (args[i] === "--gateway-port" && args[i + 1]) {
724
+ ocGatewayPort = parseInt(args[i + 1], 10);
725
+ }
615
726
  if (args[i] === "--help" || args[i] === "-h") {
616
727
  console.log(`
617
728
  ${BOLD}openclaw-navigator${RESET} — One-command bridge + tunnel for Navigator
@@ -623,12 +734,24 @@ ${BOLD}Usage:${RESET}
623
734
  npx openclaw-navigator --port 18790 Custom port
624
735
 
625
736
  ${BOLD}Options:${RESET}
626
- --port <port> Bridge server port (default: 18790)
627
- --mcp Also start the MCP server (stdio) for OpenClaw agent
628
- --ui-port <port> OC web UI port to reverse-proxy (default: 4000)
629
- --no-tunnel Skip auto-tunnel, use SSH or LAN instead
630
- --bind <host> Bind address (default: 127.0.0.1)
631
- --help Show this help
737
+ --port <port> Bridge server port (default: 18790)
738
+ --mcp Also start the MCP server (stdio) for OpenClaw agent
739
+ --ui-port <port> OC web UI port to reverse-proxy (default: 4000)
740
+ --gateway-port <port> OC gateway port for API + WebSocket (default: 18789)
741
+ --no-tunnel Skip auto-tunnel, use SSH or LAN instead
742
+ --bind <host> Bind address (default: 127.0.0.1)
743
+ --help Show this help
744
+
745
+ ${BOLD}Routing (through Cloudflare tunnel):${RESET}
746
+ /ui/* → localhost:<ui-port> Web UI (login page, dashboard)
747
+ /api/* → localhost:<gateway-port> Sessions API, chat endpoints
748
+ /ws, WebSocket → localhost:<gateway-port> Real-time streaming, chat events
749
+ /health → bridge itself Health check
750
+ /navigator/* → bridge itself Navigator control endpoints
751
+
752
+ ${BOLD}Environment variables:${RESET}
753
+ OPENCLAW_UI_PORT=4000 Where the web UI runs
754
+ OPENCLAW_GATEWAY_PORT=18789 Where the OC gateway runs
632
755
 
633
756
  ${BOLD}How it works:${RESET}
634
757
  1. Starts a bridge server on localhost
@@ -641,6 +764,8 @@ ${BOLD}How it works:${RESET}
641
764
  }
642
765
  }
643
766
 
767
+ bridgePort = port; // Expose for status endpoint
768
+
644
769
  heading("🧭 Navigator Bridge");
645
770
  info("One-command bridge + tunnel for the Navigator browser\n");
646
771
 
@@ -658,6 +783,51 @@ ${BOLD}How it works:${RESET}
658
783
  server.listen(port, bindHost, () => resolve());
659
784
  });
660
785
 
786
+ // ── WebSocket upgrade proxy → OC Gateway (localhost:ocGatewayPort) ───
787
+ // Proxies WebSocket connections so Navigator can stream chat events
788
+ server.on("upgrade", (req, socket, head) => {
789
+ const reqUrl = new URL(req.url ?? "/", "http://localhost");
790
+ const reqPath = reqUrl.pathname;
791
+
792
+ // Only proxy WebSocket paths intended for the OC gateway
793
+ if (reqPath === "/ws" || reqPath.startsWith("/ws/") || reqPath.startsWith("/api/") || reqPath === "/") {
794
+ const targetPath = `${reqPath}${reqUrl.search}`;
795
+
796
+ const proxy = netConnect(ocGatewayPort, "127.0.0.1", () => {
797
+ // Forward the original HTTP upgrade request over the TCP socket
798
+ const upgradeReq =
799
+ `${req.method} ${targetPath} HTTP/${req.httpVersion}\r\n` +
800
+ Object.entries(req.headers)
801
+ .map(([k, v]) => `${k}: ${v}`)
802
+ .join("\r\n") +
803
+ "\r\n\r\n";
804
+ proxy.write(upgradeReq);
805
+ if (head && head.length > 0) {
806
+ proxy.write(head);
807
+ }
808
+ // Pipe bidirectionally
809
+ socket.pipe(proxy);
810
+ proxy.pipe(socket);
811
+ });
812
+
813
+ proxy.on("error", (err) => {
814
+ console.log(` ${DIM}WebSocket proxy error: ${err.message}${RESET}`);
815
+ socket.destroy();
816
+ });
817
+
818
+ socket.on("error", () => {
819
+ proxy.destroy();
820
+ });
821
+
822
+ socket.on("close", () => {
823
+ proxy.destroy();
824
+ });
825
+ } else {
826
+ // Not a proxied path — reject
827
+ socket.destroy();
828
+ }
829
+ });
830
+
661
831
  ok(`Bridge server running on ${bindHost}:${port}`);
662
832
 
663
833
  // ── Step 2: Set up connectivity ───────────────────────────────────────
@@ -746,10 +916,42 @@ ${BOLD}How it works:${RESET}
746
916
  info(` Token: ${token}`);
747
917
  }
748
918
 
749
- // ── Show OC Web UI access ────────────────────────────────────────────
919
+ // ── Show OC Web UI access + routing info ─────────────────────────────
750
920
  const uiURL = tunnelURL ? `${tunnelURL}/ui/` : `http://localhost:${port}/ui/`;
751
921
  console.log(` ${BOLD}OC Web UI:${RESET} ${CYAN}${uiURL}${RESET}`);
752
- info(` (reverse-proxies to localhost:${ocUIPort} make sure OC gateway is running)`);
922
+ info(` /ui/* → localhost:${ocUIPort} (web UI)`);
923
+ info(` /api/* → localhost:${ocGatewayPort} (gateway API)`);
924
+ info(` /ws → localhost:${ocGatewayPort} (WebSocket)`);
925
+
926
+ // ── Startup health checks ──────────────────────────────────────────
927
+ console.log("");
928
+ const checkPort = (label, checkPort, checkPath) => {
929
+ return new Promise((resolve) => {
930
+ const probe = httpRequest(
931
+ { hostname: "127.0.0.1", port: checkPort, path: checkPath, method: "HEAD", timeout: 2000 },
932
+ (probeRes) => {
933
+ probeRes.resume();
934
+ ok(`${label} reachable on port ${checkPort}`);
935
+ resolve(true);
936
+ },
937
+ );
938
+ probe.on("error", () => {
939
+ warn(`${label} NOT reachable on port ${checkPort} — start it before using the tunnel`);
940
+ resolve(false);
941
+ });
942
+ probe.on("timeout", () => {
943
+ probe.destroy();
944
+ warn(`${label} timed out on port ${checkPort}`);
945
+ resolve(false);
946
+ });
947
+ probe.end();
948
+ });
949
+ };
950
+
951
+ await Promise.all([
952
+ checkPort("OC Web UI", ocUIPort, "/"),
953
+ checkPort("OC Gateway", ocGatewayPort, "/health"),
954
+ ]);
753
955
 
754
956
  console.log("");
755
957
  console.log(`${BOLD}${GREEN}Bridge is running.${RESET} Waiting for Navigator to connect...`);
@@ -852,8 +1054,106 @@ ${BOLD}How it works:${RESET}
852
1054
 
853
1055
  mkdirSync(mcporterDir, { recursive: true });
854
1056
  writeFileSync(mcporterConfigPath, JSON.stringify(mcporterConfig, null, 2) + "\n", "utf8");
855
- ok("Registered MCP server with mcporter (15 browser tools available)");
1057
+ ok("Registered MCP server with mcporter (16 browser tools available)");
856
1058
  info(` Config: ${mcporterConfigPath}`);
1059
+
1060
+ // Step 3: Install the navigator-bridge skill so the OC agent knows about all tools
1061
+ // Try multiple known skill locations
1062
+ const skillLocations = [
1063
+ "/opt/homebrew/lib/node_modules/openclaw/skills/navigator-bridge",
1064
+ join(homedir(), ".openclaw/skills/navigator-bridge"),
1065
+ ];
1066
+ // Find the first parent that exists, or use the homebrew path
1067
+ let skillDir = skillLocations[0]; // default
1068
+ for (const loc of skillLocations) {
1069
+ const parent = dirname(loc);
1070
+ if (existsSync(parent)) {
1071
+ skillDir = loc;
1072
+ break;
1073
+ }
1074
+ }
1075
+ const skillPath = join(skillDir, "SKILL.md");
1076
+ const BT = "```"; // backtick triple for markdown code blocks
1077
+ const skillContent = [
1078
+ "---",
1079
+ "name: navigator-bridge",
1080
+ "description: Control the remote Navigator browser and access the OC Web UI via MCP tools. Navigate to URLs, click elements, fill forms, read page content, execute JavaScript, manage tabs, and open the OC dashboard.",
1081
+ "metadata:",
1082
+ ' { "openclaw": { "emoji": "🌐", "requires": { "bins": ["mcporter"] } } }',
1083
+ "---",
1084
+ "",
1085
+ "# Navigator Bridge (Remote Browser Control + OC Web UI)",
1086
+ "",
1087
+ "Control the **remote** Navigator browser through MCP tools via mcporter.",
1088
+ "",
1089
+ "**Important:** This controls the REMOTE Navigator instance connected via pairing code, NOT local Navigator.",
1090
+ "",
1091
+ "## Quick start",
1092
+ "",
1093
+ BT + "bash",
1094
+ "# Get the OC Web UI URL and open it in Navigator",
1095
+ "mcporter call navigator.navigator_get_ui_url",
1096
+ "mcporter call navigator.navigator_navigate url=<uiURL from above>",
1097
+ "",
1098
+ "# Check connection status",
1099
+ "mcporter call navigator.navigator_status",
1100
+ BT,
1101
+ "",
1102
+ "## All 16 tools",
1103
+ "",
1104
+ "| Tool | What it does |",
1105
+ "|------|-------------|",
1106
+ "| `navigator_status` | Bridge status, connection, tunnel URL |",
1107
+ "| `navigator_get_ui_url` | Get the OC Web UI URL (tunnel + /ui/) |",
1108
+ "| `navigator_navigate` | Go to a URL |",
1109
+ "| `navigator_open_tab` | Open new tab |",
1110
+ "| `navigator_close_tab` | Close a tab |",
1111
+ "| `navigator_list_tabs` | List all open tabs |",
1112
+ "| `navigator_snapshot` | Full browser state snapshot |",
1113
+ "| `navigator_get_text` | Read page text content |",
1114
+ "| `navigator_get_html` | Get full page HTML |",
1115
+ "| `navigator_click` | Click element by CSS selector |",
1116
+ "| `navigator_fill` | Fill input field |",
1117
+ "| `navigator_submit` | Submit form |",
1118
+ "| `navigator_scroll` | Scroll page |",
1119
+ "| `navigator_execute_js` | Run arbitrary JavaScript |",
1120
+ "| `navigator_query_element` | Inspect DOM element |",
1121
+ "| `navigator_wait_ready` | Wait for page load |",
1122
+ "",
1123
+ "## Usage",
1124
+ "",
1125
+ BT + "bash",
1126
+ "mcporter call navigator.<tool_name> param=value",
1127
+ BT,
1128
+ "",
1129
+ "## Common workflows",
1130
+ "",
1131
+ "### Open OC dashboard in Navigator",
1132
+ BT + "bash",
1133
+ "mcporter call navigator.navigator_get_ui_url",
1134
+ "mcporter call navigator.navigator_navigate url=<uiURL>",
1135
+ "mcporter call navigator.navigator_wait_ready",
1136
+ BT,
1137
+ "",
1138
+ "### Browse and read a website",
1139
+ BT + "bash",
1140
+ "mcporter call navigator.navigator_navigate url=https://example.com",
1141
+ "mcporter call navigator.navigator_wait_ready",
1142
+ "mcporter call navigator.navigator_get_text",
1143
+ BT,
1144
+ "",
1145
+ "### Fill and submit a form",
1146
+ BT + "bash",
1147
+ 'mcporter call navigator.navigator_fill selector="#email" value="user@example.com"',
1148
+ 'mcporter call navigator.navigator_click selector="#submit"',
1149
+ "mcporter call navigator.navigator_wait_ready",
1150
+ BT,
1151
+ "",
1152
+ ].join("\n");
1153
+ mkdirSync(skillDir, { recursive: true });
1154
+ writeFileSync(skillPath, skillContent, "utf8");
1155
+ ok("Installed navigator-bridge skill for OC agent");
1156
+ info(` Skill: ${skillPath}`);
857
1157
  } catch (err) {
858
1158
  warn(`mcporter registration failed: ${err.message}`);
859
1159
  info(" You can manually configure mcporter for Navigator MCP");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "4.3.8",
3
+ "version": "4.4.0",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",