openclaw-navigator 5.0.2 → 5.2.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 (3) hide show
  1. package/cli.mjs +164 -259
  2. package/mcp.mjs +94 -29
  3. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * openclaw-navigator v4.1.0
4
+ * openclaw-navigator v5.2.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
+ * Chat and WebSocket are transparently proxied to the OC gateway (port 18789).
11
+ * The web UI at port 4000 is also proxied — no local build needed.
10
12
  *
11
13
  * Usage:
12
14
  * npx openclaw-navigator Auto-tunnel (default)
@@ -80,7 +82,9 @@ function loadBridgeIdentity() {
80
82
  if (data.pairingCode && data.token) {
81
83
  return data;
82
84
  }
83
- } catch { /* first run */ }
85
+ } catch {
86
+ /* first run */
87
+ }
84
88
  return null;
85
89
  }
86
90
 
@@ -112,7 +116,15 @@ function appendJSONL(filePath, record) {
112
116
  function readJSONL(filePath, limit = 200) {
113
117
  try {
114
118
  const lines = readFileSync(filePath, "utf8").trim().split("\n").filter(Boolean);
115
- const parsed = lines.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
119
+ const parsed = lines
120
+ .map((l) => {
121
+ try {
122
+ return JSON.parse(l);
123
+ } catch {
124
+ return null;
125
+ }
126
+ })
127
+ .filter(Boolean);
116
128
  return limit > 0 ? parsed.slice(-limit) : parsed;
117
129
  } catch {
118
130
  return [];
@@ -132,146 +144,6 @@ function writeJSON(filePath, data) {
132
144
  writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
133
145
  }
134
146
 
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
-
275
147
  // Pairing code state
276
148
  let pairingCode = null;
277
149
  let pairingData = null;
@@ -408,7 +280,9 @@ function sendJSON(res, status, body) {
408
280
 
409
281
  function validateBridgeAuth(req) {
410
282
  const authHeader = req.headers["authorization"];
411
- if (!authHeader) return false;
283
+ if (!authHeader) {
284
+ return false;
285
+ }
412
286
  const token = authHeader.replace(/^Bearer\s+/i, "");
413
287
  return validTokens.has(token);
414
288
  }
@@ -446,7 +320,7 @@ function handleRequest(req, res) {
446
320
  routing: {
447
321
  "/ui/*": `localhost:${ocUIPort}`,
448
322
  "/api/*": `localhost:${ocGatewayPort}`,
449
- "/ws": `localhost:${ocGatewayPort}`,
323
+ "/ws": `localhost:${ocGatewayPort} (WebSocket proxy)`,
450
324
  },
451
325
  tunnel: activeTunnelURL
452
326
  ? {
@@ -463,7 +337,11 @@ function handleRequest(req, res) {
463
337
  // ── GET /navigator/commands ──
464
338
  if (req.method === "GET" && path === "/navigator/commands") {
465
339
  if (!validateBridgeAuth(req)) {
466
- sendJSON(res, 401, { ok: false, error: "unauthorized", hint: "Include Authorization: Bearer <token> header" });
340
+ sendJSON(res, 401, {
341
+ ok: false,
342
+ error: "unauthorized",
343
+ hint: "Include Authorization: Bearer <token> header",
344
+ });
467
345
  return;
468
346
  }
469
347
  if (!bridgeState.connected) {
@@ -484,7 +362,11 @@ function handleRequest(req, res) {
484
362
  // ── POST /navigator/events ──
485
363
  if (req.method === "POST" && path === "/navigator/events") {
486
364
  if (!validateBridgeAuth(req)) {
487
- sendJSON(res, 401, { ok: false, error: "unauthorized", hint: "Include Authorization: Bearer <token> header" });
365
+ sendJSON(res, 401, {
366
+ ok: false,
367
+ error: "unauthorized",
368
+ hint: "Include Authorization: Bearer <token> header",
369
+ });
488
370
  return;
489
371
  }
490
372
  readBody(req)
@@ -571,7 +453,11 @@ function handleRequest(req, res) {
571
453
  // ── POST /navigator/command ──
572
454
  if (req.method === "POST" && path === "/navigator/command") {
573
455
  if (!validateBridgeAuth(req)) {
574
- sendJSON(res, 401, { ok: false, error: "unauthorized", hint: "Include Authorization: Bearer <token> header" });
456
+ sendJSON(res, 401, {
457
+ ok: false,
458
+ error: "unauthorized",
459
+ hint: "Include Authorization: Bearer <token> header",
460
+ });
575
461
  return;
576
462
  }
577
463
  readBody(req)
@@ -894,9 +780,8 @@ function handleRequest(req, res) {
894
780
  proxyReq.on("error", (err) => {
895
781
  sendJSON(res, 502, {
896
782
  ok: false,
897
- error: `OC Web UI not reachable on port ${ocUIPort}`,
783
+ error: `OC Web UI not reachable on port ${ocUIPort} — make sure the OC gateway is running`,
898
784
  detail: err.message,
899
- 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`,
900
785
  });
901
786
  });
902
787
 
@@ -906,7 +791,7 @@ function handleRequest(req, res) {
906
791
  }
907
792
 
908
793
  // ── Reverse proxy: /api/* → OC Gateway (localhost:ocGatewayPort) ─────
909
- // Keeps /api/ prefix intact so /api/sessions/send localhost:18789/api/sessions/send
794
+ // Keeps /api/ prefix intact all API calls including chat go to the gateway.
910
795
  if (path.startsWith("/api/")) {
911
796
  const targetURL = `${path}${url.search}`;
912
797
 
@@ -935,7 +820,10 @@ function handleRequest(req, res) {
935
820
  ok: false,
936
821
  error: `OC Gateway not reachable on port ${ocGatewayPort}`,
937
822
  detail: err.message,
938
- hint: "Make sure the OC gateway is running on port " + ocGatewayPort + " (openclaw gateway start)",
823
+ hint:
824
+ "Make sure the OC gateway is running on port " +
825
+ ocGatewayPort +
826
+ " (openclaw gateway start)",
939
827
  });
940
828
  });
941
829
 
@@ -1073,12 +961,9 @@ async function main() {
1073
961
  let noTunnel = false;
1074
962
  let withMcp = false;
1075
963
  let pm2Setup = false;
1076
- let tunnelToken = null; // For named tunnels (Cloudflare)
964
+ let tunnelToken = null; // For named tunnels (Cloudflare)
1077
965
  let tunnelHostname = null; // For named tunnels (stable URL)
1078
966
  let freshIdentity = false; // --new-code: force new pairing code
1079
- let setupUIFlag = false;
1080
- let updateUIFlag = false;
1081
- let noUIFlag = false;
1082
967
 
1083
968
  for (let i = 0; i < args.length; i++) {
1084
969
  if (args[i] === "--port" && args[i + 1]) {
@@ -1111,15 +996,6 @@ async function main() {
1111
996
  if (args[i] === "--new-code") {
1112
997
  freshIdentity = true;
1113
998
  }
1114
- if (args[i] === "--setup-ui") {
1115
- setupUIFlag = true;
1116
- }
1117
- if (args[i] === "--update-ui") {
1118
- updateUIFlag = true;
1119
- }
1120
- if (args[i] === "--no-ui") {
1121
- noUIFlag = true;
1122
- }
1123
999
  if (args[i] === "--help" || args[i] === "-h") {
1124
1000
  console.log(`
1125
1001
  ${BOLD}openclaw-navigator${RESET} — One-command bridge + tunnel for Navigator
@@ -1138,9 +1014,6 @@ ${BOLD}Options:${RESET}
1138
1014
  --no-tunnel Skip auto-tunnel, use SSH or LAN instead
1139
1015
  --bind <host> Bind address (default: 127.0.0.1)
1140
1016
  --new-code Force a new pairing code (discard saved identity)
1141
- --setup-ui Force (re)install + build OC Web UI
1142
- --update-ui Pull latest UI changes and rebuild
1143
- --no-ui Don't auto-start the web UI
1144
1017
  --help Show this help
1145
1018
 
1146
1019
  ${BOLD}Stability (recommended for production):${RESET}
@@ -1149,11 +1022,11 @@ ${BOLD}Stability (recommended for production):${RESET}
1149
1022
  --tunnel-hostname <host> Hostname for named tunnel (e.g. nav.yourdomain.com)
1150
1023
 
1151
1024
  ${BOLD}Routing (through Cloudflare tunnel):${RESET}
1152
- /ui/* → localhost:<ui-port> Web UI (login page, dashboard)
1153
- /api/* → localhost:<gateway-port> Sessions API, chat endpoints
1154
- /ws, WebSocket → localhost:<gateway-port> Real-time streaming, chat events
1155
- /health → bridge itself Health check
1156
- /navigator/* → bridge itself Navigator control endpoints
1025
+ /ui/* → localhost:<ui-port> Web UI (proxied to OC gateway's UI server)
1026
+ /api/* → localhost:<gateway-port> OC gateway API (chat, sessions, etc.)
1027
+ /ws, WebSocket → localhost:<gateway-port> WebSocket proxy to OC gateway
1028
+ /health → bridge itself Health check
1029
+ /navigator/* → bridge itself Navigator control endpoints
1157
1030
 
1158
1031
  ${BOLD}Environment variables:${RESET}
1159
1032
  OPENCLAW_UI_PORT=4000 Where the web UI runs
@@ -1176,7 +1049,11 @@ ${BOLD}How it works:${RESET}
1176
1049
  if (pm2Setup) {
1177
1050
  const { execSync: findNode } = await import("node:child_process");
1178
1051
  let npxPath;
1179
- try { npxPath = findNode("which npx", { encoding: "utf8" }).trim(); } catch { npxPath = "npx"; }
1052
+ try {
1053
+ npxPath = findNode("which npx", { encoding: "utf8" }).trim();
1054
+ } catch {
1055
+ npxPath = "npx";
1056
+ }
1180
1057
 
1181
1058
  const ecosystemContent = `// PM2 ecosystem config for openclaw-navigator bridge
1182
1059
  // Generated by: npx openclaw-navigator --pm2-setup
@@ -1185,7 +1062,7 @@ module.exports = {
1185
1062
  apps: [{
1186
1063
  name: "openclaw-navigator",
1187
1064
  script: "${npxPath}",
1188
- args: "openclaw-navigator@latest --mcp --no-ui --port ${port}",
1065
+ args: "openclaw-navigator@latest --mcp --port ${port}",
1189
1066
  cwd: "${homedir()}",
1190
1067
  autorestart: true,
1191
1068
  max_restarts: 50,
@@ -1212,7 +1089,9 @@ module.exports = {
1212
1089
  console.log(` ${CYAN}npm install -g pm2${RESET} ${DIM}(if not installed)${RESET}`);
1213
1090
  console.log(` ${CYAN}pm2 start ecosystem.config.cjs${RESET}`);
1214
1091
  console.log(` ${CYAN}pm2 save${RESET} ${DIM}(auto-start on boot)${RESET}`);
1215
- console.log(` ${CYAN}pm2 startup${RESET} ${DIM}(install system startup hook)${RESET}`);
1092
+ console.log(
1093
+ ` ${CYAN}pm2 startup${RESET} ${DIM}(install system startup hook)${RESET}`,
1094
+ );
1216
1095
  console.log("");
1217
1096
  console.log(`${BOLD}Useful PM2 commands:${RESET}`);
1218
1097
  console.log(` ${CYAN}pm2 logs openclaw-navigator${RESET} ${DIM}(view logs)${RESET}`);
@@ -1239,49 +1118,62 @@ module.exports = {
1239
1118
  server.listen(port, bindHost, () => resolve());
1240
1119
  });
1241
1120
 
1242
- // ── WebSocket upgrade proxy OC Gateway (localhost:ocGatewayPort) ───
1243
- // Proxies WebSocket connections so Navigator can stream chat events
1121
+ // ── WebSocket proxy forward /ws connections to OC gateway ─────────
1122
+ // The bridge is a transparent relay: Navigator connects here, we pipe to
1123
+ // the OC gateway WebSocket at ws://localhost:ocGatewayPort/ws.
1244
1124
  server.on("upgrade", (req, socket, head) => {
1245
1125
  const reqUrl = new URL(req.url ?? "/", "http://localhost");
1246
1126
  const reqPath = reqUrl.pathname;
1247
1127
 
1248
- // Only proxy WebSocket paths intended for the OC gateway
1249
- if (reqPath === "/ws" || reqPath.startsWith("/ws/") || reqPath.startsWith("/api/") || reqPath === "/") {
1250
- const targetPath = `${reqPath}${reqUrl.search}`;
1251
-
1252
- const proxy = netConnect(ocGatewayPort, "127.0.0.1", () => {
1253
- // Forward the original HTTP upgrade request over the TCP socket
1254
- const upgradeReq =
1255
- `${req.method} ${targetPath} HTTP/${req.httpVersion}\r\n` +
1256
- Object.entries(req.headers)
1257
- .map(([k, v]) => `${k}: ${v}`)
1258
- .join("\r\n") +
1259
- "\r\n\r\n";
1260
- proxy.write(upgradeReq);
1261
- if (head && head.length > 0) {
1262
- proxy.write(head);
1263
- }
1264
- // Pipe bidirectionally
1265
- socket.pipe(proxy);
1266
- proxy.pipe(socket);
1267
- });
1268
-
1269
- proxy.on("error", (err) => {
1270
- console.log(` ${DIM}WebSocket proxy error: ${err.message}${RESET}`);
1271
- socket.destroy();
1272
- });
1128
+ // Only handle /ws paths
1129
+ if (reqPath !== "/ws" && !reqPath.startsWith("/ws/")) {
1130
+ socket.destroy();
1131
+ return;
1132
+ }
1273
1133
 
1274
- socket.on("error", () => {
1275
- proxy.destroy();
1276
- });
1134
+ // Connect to the gateway WebSocket
1135
+ const gwSocket = netConnect(ocGatewayPort, "127.0.0.1", () => {
1136
+ // Forward the HTTP upgrade request to the gateway
1137
+ const upgradeHeaders = [
1138
+ `${req.method} ${req.url} HTTP/${req.httpVersion}`,
1139
+ ...Object.entries(req.headers).map(([k, v]) => `${k}: ${v}`),
1140
+ "",
1141
+ "",
1142
+ ].join("\r\n");
1143
+ gwSocket.write(upgradeHeaders);
1144
+ if (head && head.length > 0) {
1145
+ gwSocket.write(head);
1146
+ }
1147
+ });
1277
1148
 
1278
- socket.on("close", () => {
1279
- proxy.destroy();
1280
- });
1281
- } else {
1282
- // Not a proxied path — reject
1149
+ gwSocket.on("error", (err) => {
1150
+ console.log(` ${DIM}WS proxy: gateway unreachable — ${err.message}${RESET}`);
1283
1151
  socket.destroy();
1284
- }
1152
+ });
1153
+
1154
+ // Once the gateway responds with 101, pipe bidirectionally
1155
+ gwSocket.once("data", (firstChunk) => {
1156
+ const response = firstChunk.toString();
1157
+ if (response.startsWith("HTTP/1.1 101")) {
1158
+ // Forward the 101 response to the client
1159
+ socket.write(firstChunk);
1160
+ // Now pipe both directions transparently
1161
+ socket.pipe(gwSocket);
1162
+ gwSocket.pipe(socket);
1163
+
1164
+ socket.on("close", () => gwSocket.destroy());
1165
+ gwSocket.on("close", () => socket.destroy());
1166
+ socket.on("error", () => gwSocket.destroy());
1167
+ gwSocket.on("error", () => socket.destroy());
1168
+
1169
+ console.log(` ${DIM}WebSocket proxied to gateway:${ocGatewayPort}${RESET}`);
1170
+ } else {
1171
+ // Gateway rejected the upgrade
1172
+ socket.write(firstChunk);
1173
+ socket.destroy();
1174
+ gwSocket.destroy();
1175
+ }
1176
+ });
1285
1177
  });
1286
1178
 
1287
1179
  ok(`Bridge server running on ${bindHost}:${port}`);
@@ -1323,14 +1215,15 @@ module.exports = {
1323
1215
  const MAX_TUNNEL_RECONNECT_DELAY = 60_000; // cap at 60s
1324
1216
 
1325
1217
  async function startOrReconnectTunnel() {
1326
- if (!cloudflaredBin) return null;
1218
+ if (!cloudflaredBin) {
1219
+ return null;
1220
+ }
1327
1221
 
1328
1222
  // Named tunnel mode (stable URL, no reconnect gymnastics needed)
1223
+ // The tunnel routing (hostname → localhost:port) is configured in
1224
+ // the Cloudflare Zero Trust dashboard, not on the CLI.
1329
1225
  if (tunnelToken) {
1330
1226
  const tunnelArgs = ["tunnel", "run", "--token", tunnelToken];
1331
- if (tunnelHostname) {
1332
- tunnelArgs.push("--url", `http://localhost:${port}`);
1333
- }
1334
1227
  const child = spawn(cloudflaredBin, tunnelArgs, {
1335
1228
  stdio: ["ignore", "pipe", "pipe"],
1336
1229
  });
@@ -1349,6 +1242,14 @@ module.exports = {
1349
1242
  gatewayURL = namedURL;
1350
1243
  pairingData.url = namedURL;
1351
1244
  ok(`Named tunnel active: ${CYAN}${namedURL}${RESET}`);
1245
+
1246
+ // Register with relay so QuickConnect pairing codes resolve
1247
+ const relayOk = await registerWithRelay(pairingCode, namedURL, token, displayName);
1248
+ if (relayOk) {
1249
+ ok("Pairing code registered with relay");
1250
+ } else {
1251
+ warn("Relay unavailable — use the deep link or tunnel URL directly");
1252
+ }
1352
1253
  }
1353
1254
  return child;
1354
1255
  }
@@ -1356,9 +1257,14 @@ module.exports = {
1356
1257
  // Quick Tunnel mode — URL changes on every start
1357
1258
  const result = await startTunnel(cloudflaredBin, port);
1358
1259
  if (!result) {
1359
- const delay = Math.min(2000 * Math.pow(2, tunnelReconnectAttempts), MAX_TUNNEL_RECONNECT_DELAY);
1260
+ const delay = Math.min(
1261
+ 2000 * Math.pow(2, tunnelReconnectAttempts),
1262
+ MAX_TUNNEL_RECONNECT_DELAY,
1263
+ );
1360
1264
  tunnelReconnectAttempts++;
1361
- warn(`Tunnel failed — retrying in ${Math.round(delay / 1000)}s (attempt ${tunnelReconnectAttempts})...`);
1265
+ warn(
1266
+ `Tunnel failed — retrying in ${Math.round(delay / 1000)}s (attempt ${tunnelReconnectAttempts})...`,
1267
+ );
1362
1268
  setTimeout(startOrReconnectTunnel, delay);
1363
1269
  return null;
1364
1270
  }
@@ -1390,7 +1296,10 @@ module.exports = {
1390
1296
  tunnelProcess = null;
1391
1297
  activeTunnelURL = null;
1392
1298
  // Exponential backoff restart
1393
- const delay = Math.min(3000 * Math.pow(2, tunnelReconnectAttempts), MAX_TUNNEL_RECONNECT_DELAY);
1299
+ const delay = Math.min(
1300
+ 3000 * Math.pow(2, tunnelReconnectAttempts),
1301
+ MAX_TUNNEL_RECONNECT_DELAY,
1302
+ );
1394
1303
  tunnelReconnectAttempts++;
1395
1304
  setTimeout(startOrReconnectTunnel, delay);
1396
1305
  });
@@ -1439,33 +1348,10 @@ module.exports = {
1439
1348
  console.log("");
1440
1349
  }
1441
1350
 
1442
- // ── OC Web UI: auto-setup + start ─────────────────────────────────────
1443
- if (!noUIFlag) {
1444
- if (setupUIFlag) {
1445
- await setupUI();
1446
- } else if (updateUIFlag) {
1447
- await updateUI();
1448
- }
1449
-
1450
- if (await isUIInstalled()) {
1451
- await startUIServer(ocUIPort);
1452
- } else if (!setupUIFlag && !noUIFlag) {
1453
- heading("OC Web UI not found — setting up automatically");
1454
- const setupOk = await setupUI();
1455
- if (setupOk) {
1456
- await startUIServer(ocUIPort);
1457
- } else {
1458
- warn("Web UI setup failed — you can retry with: npx openclaw-navigator --setup-ui");
1459
- warn("The bridge will still work, but /ui/* won't serve the dashboard");
1460
- }
1461
- }
1462
- }
1463
-
1464
1351
  // ── Step 4: Register initial pairing code with relay ────────────────
1465
- if (tunnelURL && !tunnelToken) {
1466
- // Already registered inside startOrReconnectTunnel()
1467
- } else if (!tunnelURL) {
1468
- // Local mode — register code for local resolution
1352
+ // Both quick-tunnel and named-tunnel register inside startOrReconnectTunnel()
1353
+ if (!tunnelURL) {
1354
+ // Local mode no relay registration needed
1469
1355
  }
1470
1356
 
1471
1357
  // ── Step 5: Show connection info ──────────────────────────────────────
@@ -1492,9 +1378,9 @@ module.exports = {
1492
1378
  // ── Show OC Web UI access + routing info ─────────────────────────────
1493
1379
  const uiURL = tunnelURL ? `${tunnelURL}/ui/` : `http://localhost:${port}/ui/`;
1494
1380
  console.log(` ${BOLD}OC Web UI:${RESET} ${CYAN}${uiURL}${RESET}`);
1495
- info(` /ui/* → localhost:${ocUIPort} (web UI)`);
1496
- info(` /api/* → localhost:${ocGatewayPort} (gateway API)`);
1497
- info(` /ws → localhost:${ocGatewayPort} (WebSocket)`);
1381
+ info(` /ui/* → localhost:${ocUIPort} (web UI proxy)`);
1382
+ info(` /api/* → localhost:${ocGatewayPort} (OC gateway)`);
1383
+ info(` /ws → localhost:${ocGatewayPort} (WebSocket proxy)`);
1498
1384
 
1499
1385
  // ── Startup health checks ──────────────────────────────────────────
1500
1386
  console.log("");
@@ -1627,7 +1513,9 @@ module.exports = {
1627
1513
 
1628
1514
  mkdirSync(mcporterDir, { recursive: true });
1629
1515
  writeFileSync(mcporterConfigPath, JSON.stringify(mcporterConfig, null, 2) + "\n", "utf8");
1630
- ok("Registered MCP server with mcporter (31 tools: 16 browser + 10 AI + 5 profiling)");
1516
+ ok(
1517
+ "Registered MCP server with mcporter (34 tools: 16 browser + 10 AI + 5 profiling + 3 chat)",
1518
+ );
1631
1519
  info(` Config: ${mcporterConfigPath}`);
1632
1520
 
1633
1521
  // Step 3: Install the navigator-bridge skill so the OC agent knows about all tools
@@ -1672,7 +1560,7 @@ module.exports = {
1672
1560
  "mcporter call navigator.navigator_status",
1673
1561
  BT,
1674
1562
  "",
1675
- "## All 31 tools",
1563
+ "## All 34 tools",
1676
1564
  "",
1677
1565
  "### Browser Control (16 tools)",
1678
1566
  "",
@@ -1720,6 +1608,14 @@ module.exports = {
1720
1608
  "| `navigator_get_user_profile` | Get aggregated user interest profile |",
1721
1609
  "| `navigator_save_user_profile` | Save/update user profile from browsing patterns |",
1722
1610
  "",
1611
+ "### Chat (3 tools)",
1612
+ "",
1613
+ "| Tool | What it does |",
1614
+ "|------|-------------|",
1615
+ "| `navigator_get_chat_messages` | Get recent messages from the Navigator chat pane |",
1616
+ "| `navigator_chat_respond` | Send a response message to the Navigator chat pane |",
1617
+ "| `navigator_chat_stream` | Stream partial text to the chat pane (typing effect) |",
1618
+ "",
1723
1619
  "## Usage",
1724
1620
  "",
1725
1621
  BT + "bash",
@@ -1814,7 +1710,7 @@ module.exports = {
1814
1710
  ' url="<page_url>" \\',
1815
1711
  ' title="<page_title>" \\',
1816
1712
  ' summary="<haiku_generated_summary>" \\',
1817
- " signals='{\"names\":[],\"interests\":[],\"services\":[],\"purchases\":[],\"intent\":[],\"topics\":[]}'",
1713
+ ' signals=\'{"names":[],"interests":[],"services":[],"purchases":[],"intent":[],"topics":[]}\'',
1818
1714
  BT,
1819
1715
  "",
1820
1716
  "### 3. Daily Profile Synthesis (You Do This — Use Opus 4.6)",
@@ -1862,7 +1758,7 @@ module.exports = {
1862
1758
  "",
1863
1759
  "- **Be smart about noise**: Ignore login pages, error pages, redirects",
1864
1760
  "- **Respect privacy**: Don't store passwords, tokens, or PII like SSNs/credit cards",
1865
- "- **Extract signal from noise**: A visit to \"Nike Air Max\" tells you about shopping interest, not just a URL",
1761
+ '- **Extract signal from noise**: A visit to "Nike Air Max" tells you about shopping interest, not just a URL',
1866
1762
  "- **Cross-reference**: If user visits Stripe docs AND Vercel, they're likely a developer building a SaaS",
1867
1763
  "- **Temporal awareness**: Morning habits vs evening habits, weekday vs weekend",
1868
1764
  "- **Don't hallucinate**: Only include signals backed by actual browsing data",
@@ -1871,7 +1767,7 @@ module.exports = {
1871
1767
  "",
1872
1768
  "- **Summarization**: Every 10 minutes while the user is actively browsing",
1873
1769
  "- **Profile synthesis**: Once daily, preferably at end of day",
1874
- "- **On demand**: User can ask \"update my profile\" or \"what do you know about me\"",
1770
+ '- **On demand**: User can ask "update my profile" or "what do you know about me"',
1875
1771
  "",
1876
1772
  ].join("\n");
1877
1773
  mkdirSync(profilerSkillDir, { recursive: true });
@@ -1908,6 +1804,19 @@ module.exports = {
1908
1804
  }
1909
1805
  const npxForPlist = join(dirname(nodeForPlist), "npx");
1910
1806
 
1807
+ // Build ProgramArguments — include named tunnel flags if set
1808
+ let tunnelPlistArgs = "";
1809
+ if (tunnelToken) {
1810
+ tunnelPlistArgs += `
1811
+ <string>--tunnel-token</string>
1812
+ <string>${tunnelToken}</string>`;
1813
+ }
1814
+ if (tunnelHostname) {
1815
+ tunnelPlistArgs += `
1816
+ <string>--tunnel-hostname</string>
1817
+ <string>${tunnelHostname}</string>`;
1818
+ }
1819
+
1911
1820
  const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
1912
1821
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1913
1822
  <plist version="1.0">
@@ -1920,7 +1829,7 @@ module.exports = {
1920
1829
  <string>openclaw-navigator@latest</string>
1921
1830
  <string>--mcp</string>
1922
1831
  <string>--port</string>
1923
- <string>${port}</string>
1832
+ <string>${port}</string>${tunnelPlistArgs}
1924
1833
  </array>
1925
1834
  <key>EnvironmentVariables</key>
1926
1835
  <dict>
@@ -1972,10 +1881,6 @@ module.exports = {
1972
1881
  // ── Graceful shutdown ─────────────────────────────────────────────────
1973
1882
  const shutdown = () => {
1974
1883
  console.log(`\n${DIM}Shutting down bridge...${RESET}`);
1975
- if (uiProcess) {
1976
- uiProcess.kill();
1977
- uiProcess = null;
1978
- }
1979
1884
  if (mcpProcess) {
1980
1885
  mcpProcess.kill();
1981
1886
  }
package/mcp.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * openclaw-navigator MCP server v5.0.1
4
+ * openclaw-navigator MCP server v5.1.0
5
5
  *
6
6
  * Exposes the Navigator bridge HTTP API as MCP tools so the OpenClaw agent
7
7
  * can control the browser natively via its tool schema.
@@ -18,8 +18,8 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
18
18
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
19
  import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
20
20
  import { readFileSync } from "node:fs";
21
- import { join } from "node:path";
22
21
  import { homedir } from "node:os";
22
+ import { join } from "node:path";
23
23
 
24
24
  // ── Configuration ─────────────────────────────────────────────────────────
25
25
 
@@ -204,7 +204,7 @@ async function sendCommand(command, payload, poll) {
204
204
  };
205
205
  }
206
206
 
207
- // ── Tool definitions (31 tools: 16 browser + 10 AI intelligence + 5 profiling) ────────────────
207
+ // ── Tool definitions (34 tools: 16 browser + 10 AI intelligence + 5 profiling + 3 chat) ────────────────
208
208
 
209
209
  const TOOLS = [
210
210
  // ── Direct HTTP ──
@@ -596,6 +596,59 @@ const TOOLS = [
596
596
  required: ["profile"],
597
597
  },
598
598
  },
599
+
600
+ // ── Chat Tools ──
601
+ {
602
+ name: "navigator_get_chat_messages",
603
+ description:
604
+ "Get recent chat messages from the Navigator chat pane. Use this to see what the user is asking.",
605
+ inputSchema: {
606
+ type: "object",
607
+ properties: {
608
+ sessionKey: {
609
+ type: "string",
610
+ description: "Chat session key (default: main)",
611
+ default: "main",
612
+ },
613
+ limit: { type: "number", description: "Max messages to return (default: 20)", default: 20 },
614
+ },
615
+ },
616
+ },
617
+ {
618
+ name: "navigator_chat_respond",
619
+ description:
620
+ "Send a response message to the Navigator chat pane. The user will see this as an agent message.",
621
+ inputSchema: {
622
+ type: "object",
623
+ properties: {
624
+ message: { type: "string", description: "The response message to send" },
625
+ sessionKey: {
626
+ type: "string",
627
+ description: "Chat session key (default: main)",
628
+ default: "main",
629
+ },
630
+ },
631
+ required: ["message"],
632
+ },
633
+ },
634
+ {
635
+ name: "navigator_chat_stream",
636
+ description:
637
+ "Stream a partial response to the Navigator chat pane (for real-time typing effect).",
638
+ inputSchema: {
639
+ type: "object",
640
+ properties: {
641
+ text: { type: "string", description: "Partial text to stream" },
642
+ sessionKey: {
643
+ type: "string",
644
+ description: "Chat session key (default: main)",
645
+ default: "main",
646
+ },
647
+ runId: { type: "string", description: "Run ID for grouping streamed chunks" },
648
+ },
649
+ required: ["text"],
650
+ },
651
+ },
599
652
  ];
600
653
 
601
654
  // ── Tool handler dispatch ─────────────────────────────────────────────────
@@ -720,9 +773,7 @@ async function handleTool(name, args) {
720
773
 
721
774
  // ── AI Browser Intelligence ──
722
775
  case "navigator_analyze_page":
723
- return jsonResult(
724
- await sendCommand("ai.analyze", {}, { commandName: "ai.analyze" }),
725
- );
776
+ return jsonResult(await sendCommand("ai.analyze", {}, { commandName: "ai.analyze" }));
726
777
 
727
778
  case "navigator_find_element":
728
779
  return jsonResult(
@@ -730,9 +781,7 @@ async function handleTool(name, args) {
730
781
  );
731
782
 
732
783
  case "navigator_is_ready":
733
- return jsonResult(
734
- await sendCommand("ai.ready", {}, { commandName: "ai.ready" }),
735
- );
784
+ return jsonResult(await sendCommand("ai.ready", {}, { commandName: "ai.ready" }));
736
785
 
737
786
  case "navigator_wait_for_element":
738
787
  return jsonResult(
@@ -754,36 +803,22 @@ async function handleTool(name, args) {
754
803
 
755
804
  case "navigator_smart_fill":
756
805
  return jsonResult(
757
- await sendCommand(
758
- "ai.fill",
759
- { data: args.data },
760
- { commandName: "ai.fill" },
761
- ),
806
+ await sendCommand("ai.fill", { data: args.data }, { commandName: "ai.fill" }),
762
807
  );
763
808
 
764
809
  case "navigator_intercept_api":
765
- return jsonResult(
766
- await sendCommand("ai.intercept", {}, { commandName: "ai.intercept" }),
767
- );
810
+ return jsonResult(await sendCommand("ai.intercept", {}, { commandName: "ai.intercept" }));
768
811
 
769
812
  case "navigator_set_cookies":
770
813
  return jsonResult(
771
- await sendCommand(
772
- "ai.cookies",
773
- { cookies: args.cookies },
774
- { commandName: "ai.cookies" },
775
- ),
814
+ await sendCommand("ai.cookies", { cookies: args.cookies }, { commandName: "ai.cookies" }),
776
815
  );
777
816
 
778
817
  case "navigator_get_performance":
779
- return jsonResult(
780
- await sendCommand("ai.metrics", {}, { commandName: "ai.metrics" }),
781
- );
818
+ return jsonResult(await sendCommand("ai.metrics", {}, { commandName: "ai.metrics" }));
782
819
 
783
820
  case "navigator_get_page_state":
784
- return jsonResult(
785
- await sendCommand("ai.state", {}, { commandName: "ai.state" }),
786
- );
821
+ return jsonResult(await sendCommand("ai.state", {}, { commandName: "ai.state" }));
787
822
 
788
823
  // ── User Profiling Tools ──
789
824
  case "navigator_get_page_visits": {
@@ -823,6 +858,36 @@ async function handleTool(name, args) {
823
858
  }),
824
859
  );
825
860
 
861
+ // ── Chat Tools ──
862
+ case "navigator_get_chat_messages": {
863
+ const sessionKey = args.sessionKey || "main";
864
+ const limit = args.limit ?? 20;
865
+ const data = await bridgeGet(
866
+ `/api/sessions/history?sessionKey=${encodeURIComponent(sessionKey)}`,
867
+ );
868
+ if (data.ok && Array.isArray(data.messages)) {
869
+ return jsonResult({ ok: true, messages: data.messages.slice(-limit) });
870
+ }
871
+ return jsonResult(data);
872
+ }
873
+
874
+ case "navigator_chat_respond":
875
+ return jsonResult(
876
+ await bridgePost("/api/sessions/respond", {
877
+ content: args.message,
878
+ sessionKey: args.sessionKey || "main",
879
+ }),
880
+ );
881
+
882
+ case "navigator_chat_stream":
883
+ return jsonResult(
884
+ await bridgePost("/api/sessions/stream", {
885
+ text: args.text,
886
+ sessionKey: args.sessionKey || "main",
887
+ runId: args.runId || null,
888
+ }),
889
+ );
890
+
826
891
  default:
827
892
  return errorResult(`Unknown tool: ${name}`);
828
893
  }
@@ -835,7 +900,7 @@ async function handleTool(name, args) {
835
900
  // ── MCP server wiring ─────────────────────────────────────────────────────
836
901
 
837
902
  const server = new Server(
838
- { name: "openclaw-navigator", version: "5.0.0" },
903
+ { name: "openclaw-navigator", version: "5.1.0" },
839
904
  { capabilities: { tools: {} } },
840
905
  );
841
906
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.0.2",
3
+ "version": "5.2.0",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",