openclaw-navigator 5.1.0 → 5.2.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 (3) hide show
  1. package/cli.mjs +175 -475
  2. package/mcp.mjs +32 -33
  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)
@@ -15,7 +17,7 @@
15
17
  */
16
18
 
17
19
  import { spawn } from "node:child_process";
18
- import { randomUUID, createHash } from "node:crypto";
20
+ import { randomUUID } from "node:crypto";
19
21
  import { existsSync, appendFileSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
20
22
  import { createServer, request as httpRequest } from "node:http";
21
23
  import { connect as netConnect } from "node:net";
@@ -57,24 +59,6 @@ const recentEvents = [];
57
59
  const MAX_EVENTS = 200;
58
60
  const validTokens = new Set();
59
61
 
60
- // ── Chat session state ────────────────────────────────────────────────────
61
- const chatSessions = new Map(); // sessionKey → { messages: [...] }
62
- const wsClients = new Set(); // connected WebSocket clients
63
-
64
- function getChatSession(key = "main") {
65
- if (!chatSessions.has(key)) {
66
- chatSessions.set(key, { messages: [] });
67
- }
68
- return chatSessions.get(key);
69
- }
70
-
71
- function broadcastToWS(event) {
72
- const data = JSON.stringify(event);
73
- for (const ws of wsClients) {
74
- try { ws.send(data); } catch {}
75
- }
76
- }
77
-
78
62
  // OC Web UI reverse proxy target (configurable via --ui-port or env)
79
63
  let ocUIPort = parseInt(process.env.OPENCLAW_UI_PORT ?? "4000", 10);
80
64
 
@@ -98,7 +82,9 @@ function loadBridgeIdentity() {
98
82
  if (data.pairingCode && data.token) {
99
83
  return data;
100
84
  }
101
- } catch { /* first run */ }
85
+ } catch {
86
+ /* first run */
87
+ }
102
88
  return null;
103
89
  }
104
90
 
@@ -130,7 +116,15 @@ function appendJSONL(filePath, record) {
130
116
  function readJSONL(filePath, limit = 200) {
131
117
  try {
132
118
  const lines = readFileSync(filePath, "utf8").trim().split("\n").filter(Boolean);
133
- 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);
134
128
  return limit > 0 ? parsed.slice(-limit) : parsed;
135
129
  } catch {
136
130
  return [];
@@ -150,146 +144,6 @@ function writeJSON(filePath, data) {
150
144
  writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
151
145
  }
152
146
 
153
- // ── OC Web UI lifecycle ──────────────────────────────────────────────────────
154
-
155
- const UI_DIR = join(homedir(), ".openclaw", "ui");
156
- const UI_REPO = "https://github.com/sandman66666/openclaw-ui.git";
157
- let uiProcess = null;
158
-
159
- async function isUIInstalled() {
160
- return existsSync(join(UI_DIR, "package.json")) && existsSync(join(UI_DIR, ".next"));
161
- }
162
-
163
- async function setupUI() {
164
- const { execSync } = await import("node:child_process");
165
-
166
- if (existsSync(join(UI_DIR, "package.json"))) {
167
- info(" UI directory exists, reinstalling...");
168
- return await buildUI();
169
- }
170
-
171
- heading("Setting up OC Web UI (first time)");
172
- info(" Cloning from GitHub...");
173
-
174
- mkdirSync(join(homedir(), ".openclaw"), { recursive: true });
175
-
176
- try {
177
- execSync(`git clone --depth 1 ${UI_REPO} "${UI_DIR}"`, {
178
- stdio: ["ignore", "pipe", "pipe"],
179
- timeout: 60000,
180
- });
181
- ok("Repository cloned");
182
- } catch (err) {
183
- fail(`Failed to clone UI repo: ${err.message}`);
184
- return false;
185
- }
186
-
187
- return await buildUI();
188
- }
189
-
190
- async function buildUI() {
191
- const { execSync } = await import("node:child_process");
192
-
193
- process.stdout.write(` ${DIM}Installing dependencies (this may take a minute)...${RESET}`);
194
- try {
195
- execSync("npm install --production=false", {
196
- cwd: UI_DIR,
197
- stdio: ["ignore", "pipe", "pipe"],
198
- timeout: 120000,
199
- env: { ...process.env, NODE_ENV: "development" },
200
- });
201
- process.stdout.write(`\r${" ".repeat(70)}\r`);
202
- ok("Dependencies installed");
203
- } catch (err) {
204
- process.stdout.write(`\r${" ".repeat(70)}\r`);
205
- fail(`npm install failed: ${err.stderr?.toString()?.slice(-200) || err.message}`);
206
- return false;
207
- }
208
-
209
- process.stdout.write(` ${DIM}Building web UI...${RESET}`);
210
- try {
211
- execSync("npx next build", {
212
- cwd: UI_DIR,
213
- stdio: ["ignore", "pipe", "pipe"],
214
- timeout: 180000,
215
- });
216
- process.stdout.write(`\r${" ".repeat(70)}\r`);
217
- ok("Web UI built successfully");
218
- return true;
219
- } catch (err) {
220
- process.stdout.write(`\r${" ".repeat(70)}\r`);
221
- fail(`Build failed: ${err.stderr?.toString()?.slice(-200) || err.message}`);
222
- return false;
223
- }
224
- }
225
-
226
- async function updateUI() {
227
- const { execSync } = await import("node:child_process");
228
-
229
- if (!existsSync(join(UI_DIR, ".git"))) {
230
- warn("UI not installed yet — running full setup instead");
231
- return await setupUI();
232
- }
233
-
234
- heading("Updating OC Web UI");
235
-
236
- try {
237
- const result = execSync("git pull --rebase origin main", {
238
- cwd: UI_DIR,
239
- encoding: "utf8",
240
- timeout: 30000,
241
- });
242
-
243
- if (result.includes("Already up to date")) {
244
- ok("Already up to date");
245
- return true;
246
- }
247
-
248
- ok("Pulled latest changes");
249
- return await buildUI();
250
- } catch (err) {
251
- fail(`Update failed: ${err.message}`);
252
- return false;
253
- }
254
- }
255
-
256
- function startUIServer(port) {
257
- if (uiProcess) {
258
- uiProcess.kill();
259
- uiProcess = null;
260
- }
261
-
262
- uiProcess = spawn("npx", ["next", "start", "-p", String(port)], {
263
- cwd: UI_DIR,
264
- stdio: ["ignore", "pipe", "pipe"],
265
- env: { ...process.env, PORT: String(port), NODE_ENV: "production" },
266
- });
267
-
268
- uiProcess.on("error", (err) => {
269
- warn(`OC Web UI failed to start: ${err.message}`);
270
- uiProcess = null;
271
- });
272
-
273
- uiProcess.on("exit", (code) => {
274
- if (code !== null && code !== 0) {
275
- warn(`OC Web UI exited with code ${code}`);
276
- }
277
- uiProcess = null;
278
- });
279
-
280
- return new Promise((resolve) => {
281
- const timer = setTimeout(() => {
282
- ok(`OC Web UI starting on port ${port} (PID ${uiProcess?.pid})`);
283
- resolve(true);
284
- }, 1500);
285
-
286
- uiProcess.on("exit", () => {
287
- clearTimeout(timer);
288
- resolve(false);
289
- });
290
- });
291
- }
292
-
293
147
  // Pairing code state
294
148
  let pairingCode = null;
295
149
  let pairingData = null;
@@ -426,7 +280,9 @@ function sendJSON(res, status, body) {
426
280
 
427
281
  function validateBridgeAuth(req) {
428
282
  const authHeader = req.headers["authorization"];
429
- if (!authHeader) return false;
283
+ if (!authHeader) {
284
+ return false;
285
+ }
430
286
  const token = authHeader.replace(/^Bearer\s+/i, "");
431
287
  return validTokens.has(token);
432
288
  }
@@ -463,9 +319,8 @@ function handleRequest(req, res) {
463
319
  },
464
320
  routing: {
465
321
  "/ui/*": `localhost:${ocUIPort}`,
466
- "/api/sessions/*": "bridge (in-process chat)",
467
- "/api/*": `localhost:${ocGatewayPort} (fallback)`,
468
- "/ws": "bridge (in-process WebSocket)",
322
+ "/api/*": `localhost:${ocGatewayPort}`,
323
+ "/ws": `localhost:${ocGatewayPort} (WebSocket proxy)`,
469
324
  },
470
325
  tunnel: activeTunnelURL
471
326
  ? {
@@ -482,7 +337,11 @@ function handleRequest(req, res) {
482
337
  // ── GET /navigator/commands ──
483
338
  if (req.method === "GET" && path === "/navigator/commands") {
484
339
  if (!validateBridgeAuth(req)) {
485
- 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
+ });
486
345
  return;
487
346
  }
488
347
  if (!bridgeState.connected) {
@@ -503,7 +362,11 @@ function handleRequest(req, res) {
503
362
  // ── POST /navigator/events ──
504
363
  if (req.method === "POST" && path === "/navigator/events") {
505
364
  if (!validateBridgeAuth(req)) {
506
- 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
+ });
507
370
  return;
508
371
  }
509
372
  readBody(req)
@@ -590,7 +453,11 @@ function handleRequest(req, res) {
590
453
  // ── POST /navigator/command ──
591
454
  if (req.method === "POST" && path === "/navigator/command") {
592
455
  if (!validateBridgeAuth(req)) {
593
- 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
+ });
594
461
  return;
595
462
  }
596
463
  readBody(req)
@@ -888,6 +755,7 @@ function handleRequest(req, res) {
888
755
  // Keep /ui prefix — Next.js basePath: "/ui" expects it
889
756
  if (path === "/ui" || path.startsWith("/ui/")) {
890
757
  const targetURL = `${path}${url.search}`;
758
+ const incomingHost = req.headers.host || "localhost";
891
759
 
892
760
  const proxyOpts = {
893
761
  hostname: "127.0.0.1",
@@ -897,15 +765,38 @@ function handleRequest(req, res) {
897
765
  headers: {
898
766
  ...req.headers,
899
767
  host: `127.0.0.1:${ocUIPort}`,
768
+ "x-forwarded-host": incomingHost,
769
+ "x-forwarded-proto": activeTunnelURL ? "https" : "http",
770
+ "x-forwarded-for": req.socket.remoteAddress || "127.0.0.1",
900
771
  },
901
772
  };
902
773
 
903
774
  const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
904
- // Forward status + headers (add CORS for cross-origin tunnel access)
905
775
  const headers = { ...proxyRes.headers };
776
+
777
+ // CORS for cross-origin tunnel access
906
778
  headers["access-control-allow-origin"] = "*";
907
779
  headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
908
- headers["access-control-allow-headers"] = "Content-Type, Authorization";
780
+ headers["access-control-allow-headers"] = "Content-Type, Authorization, Cookie";
781
+ headers["access-control-allow-credentials"] = "true";
782
+
783
+ // Fix redirects — rewrite Location from localhost:4000 to tunnel URL
784
+ if (headers.location) {
785
+ headers.location = headers.location
786
+ .replace(`http://127.0.0.1:${ocUIPort}`, "")
787
+ .replace(`http://localhost:${ocUIPort}`, "");
788
+ }
789
+
790
+ // Fix cookies — remove domain restriction so they work through tunnel
791
+ if (headers["set-cookie"]) {
792
+ const cookies = Array.isArray(headers["set-cookie"])
793
+ ? headers["set-cookie"]
794
+ : [headers["set-cookie"]];
795
+ headers["set-cookie"] = cookies.map((c) =>
796
+ c.replace(/;\s*domain=[^;]*/gi, "").replace(/;\s*secure/gi, ""),
797
+ );
798
+ }
799
+
909
800
  res.writeHead(proxyRes.statusCode ?? 502, headers);
910
801
  proxyRes.pipe(res, { end: true });
911
802
  });
@@ -913,9 +804,8 @@ function handleRequest(req, res) {
913
804
  proxyReq.on("error", (err) => {
914
805
  sendJSON(res, 502, {
915
806
  ok: false,
916
- error: `OC Web UI not reachable on port ${ocUIPort}`,
807
+ error: `OC Web UI not reachable on port ${ocUIPort} — make sure the OC gateway is running`,
917
808
  detail: err.message,
918
- 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`,
919
809
  });
920
810
  });
921
811
 
@@ -924,117 +814,8 @@ function handleRequest(req, res) {
924
814
  return;
925
815
  }
926
816
 
927
- // ── POST /api/sessions/send — Chat: receive user message ──
928
- if (req.method === "POST" && path === "/api/sessions/send") {
929
- readBody(req).then(raw => {
930
- const body = JSON.parse(raw);
931
- const sessionKey = body.sessionKey || "main";
932
- const message = body.message;
933
- if (!message) {
934
- sendJSON(res, 400, { ok: false, error: "Missing 'message'" });
935
- return;
936
- }
937
-
938
- const session = getChatSession(sessionKey);
939
- const userMsg = {
940
- role: "user",
941
- content: message,
942
- timestamp: Date.now(),
943
- id: randomUUID(),
944
- };
945
- session.messages.push(userMsg);
946
-
947
- // Also push as a bridge event so MCP tools can see it
948
- recentEvents.push({
949
- type: "chat.user_message",
950
- sessionKey,
951
- message,
952
- messageId: userMsg.id,
953
- timestamp: userMsg.timestamp,
954
- });
955
- if (recentEvents.length > MAX_EVENTS) recentEvents.shift();
956
-
957
- // Broadcast to WebSocket clients
958
- broadcastToWS({ type: "chat.user_message", sessionKey, message, messageId: userMsg.id });
959
-
960
- sendJSON(res, 200, { ok: true, messageId: userMsg.id });
961
- }).catch(() => {
962
- sendJSON(res, 400, { ok: false, error: "Invalid JSON" });
963
- });
964
- return;
965
- }
966
-
967
- // ── GET /api/sessions/history — Chat: fetch history ──
968
- if (req.method === "GET" && path === "/api/sessions/history") {
969
- const sessionKey = url.searchParams.get("sessionKey") || "main";
970
- const session = getChatSession(sessionKey);
971
- sendJSON(res, 200, { ok: true, messages: session.messages });
972
- return;
973
- }
974
-
975
- // ── POST /api/sessions/respond — Chat: agent sends response ──
976
- // Used by MCP tools to send agent responses to the chat
977
- if (req.method === "POST" && path === "/api/sessions/respond") {
978
- readBody(req).then(raw => {
979
- const body = JSON.parse(raw);
980
- const sessionKey = body.sessionKey || "main";
981
- const content = body.content || body.message;
982
- if (!content) {
983
- sendJSON(res, 400, { ok: false, error: "Missing 'content'" });
984
- return;
985
- }
986
-
987
- const session = getChatSession(sessionKey);
988
- const assistantMsg = {
989
- role: "assistant",
990
- content,
991
- timestamp: Date.now(),
992
- id: randomUUID(),
993
- };
994
- session.messages.push(assistantMsg);
995
-
996
- // Broadcast final response via WebSocket
997
- broadcastToWS({
998
- type: "chat.final",
999
- text: content,
1000
- content,
1001
- runId: body.runId || null,
1002
- sessionKey,
1003
- });
1004
-
1005
- sendJSON(res, 200, { ok: true, messageId: assistantMsg.id });
1006
- }).catch(() => {
1007
- sendJSON(res, 400, { ok: false, error: "Invalid JSON" });
1008
- });
1009
- return;
1010
- }
1011
-
1012
- // ── POST /api/sessions/stream — Chat: agent streams partial text ──
1013
- if (req.method === "POST" && path === "/api/sessions/stream") {
1014
- readBody(req).then(raw => {
1015
- const body = JSON.parse(raw);
1016
- const text = body.text || body.delta || "";
1017
- const runId = body.runId || null;
1018
- const sessionKey = body.sessionKey || "main";
1019
-
1020
- broadcastToWS({
1021
- type: "chat.delta",
1022
- text,
1023
- delta: text,
1024
- runId,
1025
- sessionKey,
1026
- });
1027
-
1028
- sendJSON(res, 200, { ok: true });
1029
- }).catch(() => {
1030
- sendJSON(res, 400, { ok: false, error: "Invalid JSON" });
1031
- });
1032
- return;
1033
- }
1034
-
1035
817
  // ── Reverse proxy: /api/* → OC Gateway (localhost:ocGatewayPort) ─────
1036
- // Keeps /api/ prefix intact so /api/sessions/send localhost:18789/api/sessions/send
1037
- // This is a fallback — specific chat paths above are handled directly.
818
+ // Keeps /api/ prefix intact all API calls including chat go to the gateway.
1038
819
  if (path.startsWith("/api/")) {
1039
820
  const targetURL = `${path}${url.search}`;
1040
821
 
@@ -1063,7 +844,10 @@ function handleRequest(req, res) {
1063
844
  ok: false,
1064
845
  error: `OC Gateway not reachable on port ${ocGatewayPort}`,
1065
846
  detail: err.message,
1066
- hint: "Make sure the OC gateway is running on port " + ocGatewayPort + " (openclaw gateway start)",
847
+ hint:
848
+ "Make sure the OC gateway is running on port " +
849
+ ocGatewayPort +
850
+ " (openclaw gateway start)",
1067
851
  });
1068
852
  });
1069
853
 
@@ -1201,12 +985,9 @@ async function main() {
1201
985
  let noTunnel = false;
1202
986
  let withMcp = false;
1203
987
  let pm2Setup = false;
1204
- let tunnelToken = null; // For named tunnels (Cloudflare)
988
+ let tunnelToken = null; // For named tunnels (Cloudflare)
1205
989
  let tunnelHostname = null; // For named tunnels (stable URL)
1206
990
  let freshIdentity = false; // --new-code: force new pairing code
1207
- let setupUIFlag = false;
1208
- let updateUIFlag = false;
1209
- let noUIFlag = false;
1210
991
 
1211
992
  for (let i = 0; i < args.length; i++) {
1212
993
  if (args[i] === "--port" && args[i + 1]) {
@@ -1239,15 +1020,6 @@ async function main() {
1239
1020
  if (args[i] === "--new-code") {
1240
1021
  freshIdentity = true;
1241
1022
  }
1242
- if (args[i] === "--setup-ui") {
1243
- setupUIFlag = true;
1244
- }
1245
- if (args[i] === "--update-ui") {
1246
- updateUIFlag = true;
1247
- }
1248
- if (args[i] === "--no-ui") {
1249
- noUIFlag = true;
1250
- }
1251
1023
  if (args[i] === "--help" || args[i] === "-h") {
1252
1024
  console.log(`
1253
1025
  ${BOLD}openclaw-navigator${RESET} — One-command bridge + tunnel for Navigator
@@ -1266,9 +1038,6 @@ ${BOLD}Options:${RESET}
1266
1038
  --no-tunnel Skip auto-tunnel, use SSH or LAN instead
1267
1039
  --bind <host> Bind address (default: 127.0.0.1)
1268
1040
  --new-code Force a new pairing code (discard saved identity)
1269
- --setup-ui Force (re)install + build OC Web UI
1270
- --update-ui Pull latest UI changes and rebuild
1271
- --no-ui Don't auto-start the web UI
1272
1041
  --help Show this help
1273
1042
 
1274
1043
  ${BOLD}Stability (recommended for production):${RESET}
@@ -1277,10 +1046,9 @@ ${BOLD}Stability (recommended for production):${RESET}
1277
1046
  --tunnel-hostname <host> Hostname for named tunnel (e.g. nav.yourdomain.com)
1278
1047
 
1279
1048
  ${BOLD}Routing (through Cloudflare tunnel):${RESET}
1280
- /ui/* → localhost:<ui-port> Web UI (login page, dashboard)
1281
- /api/sessions/* bridge (in-process) Chat send/history/respond/stream
1282
- /ws, WebSocket → bridge (in-process) Real-time chat events
1283
- /api/* (other) → localhost:<gateway-port> Gateway API fallback
1049
+ /ui/* → localhost:<ui-port> Web UI (proxied to OC gateway's UI server)
1050
+ /api/* localhost:<gateway-port> OC gateway API (chat, sessions, etc.)
1051
+ /ws, WebSocket → localhost:<gateway-port> WebSocket proxy to OC gateway
1284
1052
  /health → bridge itself Health check
1285
1053
  /navigator/* → bridge itself Navigator control endpoints
1286
1054
 
@@ -1305,7 +1073,11 @@ ${BOLD}How it works:${RESET}
1305
1073
  if (pm2Setup) {
1306
1074
  const { execSync: findNode } = await import("node:child_process");
1307
1075
  let npxPath;
1308
- try { npxPath = findNode("which npx", { encoding: "utf8" }).trim(); } catch { npxPath = "npx"; }
1076
+ try {
1077
+ npxPath = findNode("which npx", { encoding: "utf8" }).trim();
1078
+ } catch {
1079
+ npxPath = "npx";
1080
+ }
1309
1081
 
1310
1082
  const ecosystemContent = `// PM2 ecosystem config for openclaw-navigator bridge
1311
1083
  // Generated by: npx openclaw-navigator --pm2-setup
@@ -1314,7 +1086,7 @@ module.exports = {
1314
1086
  apps: [{
1315
1087
  name: "openclaw-navigator",
1316
1088
  script: "${npxPath}",
1317
- args: "openclaw-navigator@latest --mcp --no-ui --port ${port}",
1089
+ args: "openclaw-navigator@latest --mcp --port ${port}",
1318
1090
  cwd: "${homedir()}",
1319
1091
  autorestart: true,
1320
1092
  max_restarts: 50,
@@ -1341,7 +1113,9 @@ module.exports = {
1341
1113
  console.log(` ${CYAN}npm install -g pm2${RESET} ${DIM}(if not installed)${RESET}`);
1342
1114
  console.log(` ${CYAN}pm2 start ecosystem.config.cjs${RESET}`);
1343
1115
  console.log(` ${CYAN}pm2 save${RESET} ${DIM}(auto-start on boot)${RESET}`);
1344
- console.log(` ${CYAN}pm2 startup${RESET} ${DIM}(install system startup hook)${RESET}`);
1116
+ console.log(
1117
+ ` ${CYAN}pm2 startup${RESET} ${DIM}(install system startup hook)${RESET}`,
1118
+ );
1345
1119
  console.log("");
1346
1120
  console.log(`${BOLD}Useful PM2 commands:${RESET}`);
1347
1121
  console.log(` ${CYAN}pm2 logs openclaw-navigator${RESET} ${DIM}(view logs)${RESET}`);
@@ -1368,141 +1142,63 @@ module.exports = {
1368
1142
  server.listen(port, bindHost, () => resolve());
1369
1143
  });
1370
1144
 
1371
- // ── WebSocket handlermanages chat connections directly ─────────────
1145
+ // ── WebSocket proxyforward /ws connections to OC gateway ─────────
1146
+ // The bridge is a transparent relay: Navigator connects to /ws here,
1147
+ // we pipe to the OC gateway WebSocket at ws://localhost:ocGatewayPort/
1148
+ // (OC Core accepts WebSocket at root /, not /ws)
1372
1149
  server.on("upgrade", (req, socket, head) => {
1373
1150
  const reqUrl = new URL(req.url ?? "/", "http://localhost");
1374
1151
  const reqPath = reqUrl.pathname;
1375
1152
 
1376
- // Only handle /ws paths
1153
+ // Only handle /ws paths (Navigator connects to /ws)
1377
1154
  if (reqPath !== "/ws" && !reqPath.startsWith("/ws/")) {
1378
1155
  socket.destroy();
1379
1156
  return;
1380
1157
  }
1381
1158
 
1382
- // Perform WebSocket handshake
1383
- const key = req.headers["sec-websocket-key"];
1384
- if (!key) {
1385
- socket.destroy();
1386
- return;
1387
- }
1388
-
1389
- const accept = createHash("sha1")
1390
- .update(key + "258EAFA5-E914-47DA-95CA-5AB5DC799C07")
1391
- .digest("base64");
1392
-
1393
- socket.write(
1394
- "HTTP/1.1 101 Switching Protocols\r\n" +
1395
- "Upgrade: websocket\r\n" +
1396
- "Connection: Upgrade\r\n" +
1397
- `Sec-WebSocket-Accept: ${accept}\r\n` +
1398
- "\r\n"
1399
- );
1400
-
1401
- // Create a minimal WebSocket wrapper
1402
- const ws = {
1403
- socket,
1404
- send(data) {
1405
- const payload = Buffer.from(data);
1406
- const frame = [];
1407
- frame.push(0x81); // text frame, FIN bit
1408
- if (payload.length < 126) {
1409
- frame.push(payload.length);
1410
- } else if (payload.length < 65536) {
1411
- frame.push(126, (payload.length >> 8) & 0xFF, payload.length & 0xFF);
1412
- } else {
1413
- frame.push(127);
1414
- for (let i = 7; i >= 0; i--) {
1415
- frame.push((payload.length >> (i * 8)) & 0xFF);
1416
- }
1417
- }
1418
- socket.write(Buffer.concat([Buffer.from(frame), payload]));
1419
- },
1420
- close() {
1421
- wsClients.delete(ws);
1422
- socket.destroy();
1423
- }
1424
- };
1425
-
1426
- wsClients.add(ws);
1427
- console.log(` ${DIM}WebSocket client connected (${wsClients.size} active)${RESET}`);
1428
-
1429
- // Send connected event
1430
- ws.send(JSON.stringify({ type: "connected" }));
1431
-
1432
- // Handle incoming WebSocket frames (simplified parser)
1433
- let frameBuffer = Buffer.alloc(0);
1434
- socket.on("data", (chunk) => {
1435
- frameBuffer = Buffer.concat([frameBuffer, chunk]);
1436
-
1437
- while (frameBuffer.length >= 2) {
1438
- const firstByte = frameBuffer[0];
1439
- const secondByte = frameBuffer[1];
1440
- const opcode = firstByte & 0x0F;
1441
- const masked = (secondByte & 0x80) !== 0;
1442
- let payloadLen = secondByte & 0x7F;
1443
- let offset = 2;
1444
-
1445
- if (payloadLen === 126) {
1446
- if (frameBuffer.length < 4) return;
1447
- payloadLen = (frameBuffer[2] << 8) | frameBuffer[3];
1448
- offset = 4;
1449
- } else if (payloadLen === 127) {
1450
- if (frameBuffer.length < 10) return;
1451
- payloadLen = 0;
1452
- for (let i = 0; i < 8; i++) {
1453
- payloadLen = payloadLen * 256 + frameBuffer[2 + i];
1454
- }
1455
- offset = 10;
1456
- }
1457
-
1458
- const maskSize = masked ? 4 : 0;
1459
- const totalFrameLen = offset + maskSize + payloadLen;
1460
- if (frameBuffer.length < totalFrameLen) return;
1461
-
1462
- let payload;
1463
- if (masked) {
1464
- const mask = frameBuffer.slice(offset, offset + 4);
1465
- payload = Buffer.alloc(payloadLen);
1466
- for (let i = 0; i < payloadLen; i++) {
1467
- payload[i] = frameBuffer[offset + 4 + i] ^ mask[i % 4];
1468
- }
1469
- } else {
1470
- payload = frameBuffer.slice(offset, offset + payloadLen);
1471
- }
1472
-
1473
- // Handle by opcode
1474
- if (opcode === 0x08) {
1475
- // Close frame
1476
- ws.close();
1477
- return;
1478
- } else if (opcode === 0x09) {
1479
- // Ping — send pong
1480
- const pongFrame = Buffer.from([0x8A, payload.length, ...payload]);
1481
- socket.write(pongFrame);
1482
- } else if (opcode === 0x01) {
1483
- // Text frame — handle as message
1484
- const text = payload.toString("utf8");
1485
- try {
1486
- const msg = JSON.parse(text);
1487
- if (msg.type === "ping") {
1488
- ws.send(JSON.stringify({ type: "pong" }));
1489
- }
1490
- } catch {
1491
- // Non-JSON message, ignore
1492
- }
1493
- }
1494
-
1495
- frameBuffer = frameBuffer.slice(totalFrameLen);
1159
+ // Connect to the gateway WebSocket
1160
+ const gwSocket = netConnect(ocGatewayPort, "127.0.0.1", () => {
1161
+ // Rewrite path: Navigator uses /ws but OC Core expects / (root)
1162
+ const gwPath = reqPath === "/ws" ? "/" : reqPath.replace(/^\/ws/, "");
1163
+ const upgradeHeaders = [
1164
+ `${req.method} ${gwPath || "/"} HTTP/${req.httpVersion}`,
1165
+ ...Object.entries(req.headers).map(([k, v]) => `${k}: ${v}`),
1166
+ "",
1167
+ "",
1168
+ ].join("\r\n");
1169
+ gwSocket.write(upgradeHeaders);
1170
+ if (head && head.length > 0) {
1171
+ gwSocket.write(head);
1496
1172
  }
1497
1173
  });
1498
1174
 
1499
- socket.on("close", () => {
1500
- wsClients.delete(ws);
1501
- console.log(` ${DIM}WebSocket client disconnected (${wsClients.size} active)${RESET}`);
1175
+ gwSocket.on("error", (err) => {
1176
+ console.log(` ${DIM}WS proxy: gateway unreachable — ${err.message}${RESET}`);
1177
+ socket.destroy();
1502
1178
  });
1503
1179
 
1504
- socket.on("error", () => {
1505
- wsClients.delete(ws);
1180
+ // Once the gateway responds with 101, pipe bidirectionally
1181
+ gwSocket.once("data", (firstChunk) => {
1182
+ const response = firstChunk.toString();
1183
+ if (response.startsWith("HTTP/1.1 101")) {
1184
+ // Forward the 101 response to the client
1185
+ socket.write(firstChunk);
1186
+ // Now pipe both directions transparently
1187
+ socket.pipe(gwSocket);
1188
+ gwSocket.pipe(socket);
1189
+
1190
+ socket.on("close", () => gwSocket.destroy());
1191
+ gwSocket.on("close", () => socket.destroy());
1192
+ socket.on("error", () => gwSocket.destroy());
1193
+ gwSocket.on("error", () => socket.destroy());
1194
+
1195
+ console.log(` ${DIM}WebSocket proxied to gateway:${ocGatewayPort}${RESET}`);
1196
+ } else {
1197
+ // Gateway rejected the upgrade
1198
+ socket.write(firstChunk);
1199
+ socket.destroy();
1200
+ gwSocket.destroy();
1201
+ }
1506
1202
  });
1507
1203
  });
1508
1204
 
@@ -1545,14 +1241,15 @@ module.exports = {
1545
1241
  const MAX_TUNNEL_RECONNECT_DELAY = 60_000; // cap at 60s
1546
1242
 
1547
1243
  async function startOrReconnectTunnel() {
1548
- if (!cloudflaredBin) return null;
1244
+ if (!cloudflaredBin) {
1245
+ return null;
1246
+ }
1549
1247
 
1550
1248
  // Named tunnel mode (stable URL, no reconnect gymnastics needed)
1249
+ // The tunnel routing (hostname → localhost:port) is configured in
1250
+ // the Cloudflare Zero Trust dashboard, not on the CLI.
1551
1251
  if (tunnelToken) {
1552
1252
  const tunnelArgs = ["tunnel", "run", "--token", tunnelToken];
1553
- if (tunnelHostname) {
1554
- tunnelArgs.push("--url", `http://localhost:${port}`);
1555
- }
1556
1253
  const child = spawn(cloudflaredBin, tunnelArgs, {
1557
1254
  stdio: ["ignore", "pipe", "pipe"],
1558
1255
  });
@@ -1571,6 +1268,14 @@ module.exports = {
1571
1268
  gatewayURL = namedURL;
1572
1269
  pairingData.url = namedURL;
1573
1270
  ok(`Named tunnel active: ${CYAN}${namedURL}${RESET}`);
1271
+
1272
+ // Register with relay so QuickConnect pairing codes resolve
1273
+ const relayOk = await registerWithRelay(pairingCode, namedURL, token, displayName);
1274
+ if (relayOk) {
1275
+ ok("Pairing code registered with relay");
1276
+ } else {
1277
+ warn("Relay unavailable — use the deep link or tunnel URL directly");
1278
+ }
1574
1279
  }
1575
1280
  return child;
1576
1281
  }
@@ -1578,9 +1283,14 @@ module.exports = {
1578
1283
  // Quick Tunnel mode — URL changes on every start
1579
1284
  const result = await startTunnel(cloudflaredBin, port);
1580
1285
  if (!result) {
1581
- const delay = Math.min(2000 * Math.pow(2, tunnelReconnectAttempts), MAX_TUNNEL_RECONNECT_DELAY);
1286
+ const delay = Math.min(
1287
+ 2000 * Math.pow(2, tunnelReconnectAttempts),
1288
+ MAX_TUNNEL_RECONNECT_DELAY,
1289
+ );
1582
1290
  tunnelReconnectAttempts++;
1583
- warn(`Tunnel failed — retrying in ${Math.round(delay / 1000)}s (attempt ${tunnelReconnectAttempts})...`);
1291
+ warn(
1292
+ `Tunnel failed — retrying in ${Math.round(delay / 1000)}s (attempt ${tunnelReconnectAttempts})...`,
1293
+ );
1584
1294
  setTimeout(startOrReconnectTunnel, delay);
1585
1295
  return null;
1586
1296
  }
@@ -1612,7 +1322,10 @@ module.exports = {
1612
1322
  tunnelProcess = null;
1613
1323
  activeTunnelURL = null;
1614
1324
  // Exponential backoff restart
1615
- const delay = Math.min(3000 * Math.pow(2, tunnelReconnectAttempts), MAX_TUNNEL_RECONNECT_DELAY);
1325
+ const delay = Math.min(
1326
+ 3000 * Math.pow(2, tunnelReconnectAttempts),
1327
+ MAX_TUNNEL_RECONNECT_DELAY,
1328
+ );
1616
1329
  tunnelReconnectAttempts++;
1617
1330
  setTimeout(startOrReconnectTunnel, delay);
1618
1331
  });
@@ -1661,33 +1374,10 @@ module.exports = {
1661
1374
  console.log("");
1662
1375
  }
1663
1376
 
1664
- // ── OC Web UI: auto-setup + start ─────────────────────────────────────
1665
- if (!noUIFlag) {
1666
- if (setupUIFlag) {
1667
- await setupUI();
1668
- } else if (updateUIFlag) {
1669
- await updateUI();
1670
- }
1671
-
1672
- if (await isUIInstalled()) {
1673
- await startUIServer(ocUIPort);
1674
- } else if (!setupUIFlag && !noUIFlag) {
1675
- heading("OC Web UI not found — setting up automatically");
1676
- const setupOk = await setupUI();
1677
- if (setupOk) {
1678
- await startUIServer(ocUIPort);
1679
- } else {
1680
- warn("Web UI setup failed — you can retry with: npx openclaw-navigator --setup-ui");
1681
- warn("The bridge will still work, but /ui/* won't serve the dashboard");
1682
- }
1683
- }
1684
- }
1685
-
1686
1377
  // ── Step 4: Register initial pairing code with relay ────────────────
1687
- if (tunnelURL && !tunnelToken) {
1688
- // Already registered inside startOrReconnectTunnel()
1689
- } else if (!tunnelURL) {
1690
- // Local mode — register code for local resolution
1378
+ // Both quick-tunnel and named-tunnel register inside startOrReconnectTunnel()
1379
+ if (!tunnelURL) {
1380
+ // Local mode no relay registration needed
1691
1381
  }
1692
1382
 
1693
1383
  // ── Step 5: Show connection info ──────────────────────────────────────
@@ -1714,10 +1404,9 @@ module.exports = {
1714
1404
  // ── Show OC Web UI access + routing info ─────────────────────────────
1715
1405
  const uiURL = tunnelURL ? `${tunnelURL}/ui/` : `http://localhost:${port}/ui/`;
1716
1406
  console.log(` ${BOLD}OC Web UI:${RESET} ${CYAN}${uiURL}${RESET}`);
1717
- info(` /ui/* → localhost:${ocUIPort} (web UI)`);
1718
- info(` /api/sessions/* bridge (in-process chat)`);
1719
- info(` /ws bridge (in-process WebSocket)`);
1720
- info(` /api/* (other) → localhost:${ocGatewayPort} (gateway fallback)`);
1407
+ info(` /ui/* → localhost:${ocUIPort} (web UI proxy)`);
1408
+ info(` /api/* localhost:${ocGatewayPort} (OC gateway)`);
1409
+ info(` /ws localhost:${ocGatewayPort} (WebSocket proxy)`);
1721
1410
 
1722
1411
  // ── Startup health checks ──────────────────────────────────────────
1723
1412
  console.log("");
@@ -1850,7 +1539,9 @@ module.exports = {
1850
1539
 
1851
1540
  mkdirSync(mcporterDir, { recursive: true });
1852
1541
  writeFileSync(mcporterConfigPath, JSON.stringify(mcporterConfig, null, 2) + "\n", "utf8");
1853
- ok("Registered MCP server with mcporter (34 tools: 16 browser + 10 AI + 5 profiling + 3 chat)");
1542
+ ok(
1543
+ "Registered MCP server with mcporter (34 tools: 16 browser + 10 AI + 5 profiling + 3 chat)",
1544
+ );
1854
1545
  info(` Config: ${mcporterConfigPath}`);
1855
1546
 
1856
1547
  // Step 3: Install the navigator-bridge skill so the OC agent knows about all tools
@@ -2045,7 +1736,7 @@ module.exports = {
2045
1736
  ' url="<page_url>" \\',
2046
1737
  ' title="<page_title>" \\',
2047
1738
  ' summary="<haiku_generated_summary>" \\',
2048
- " signals='{\"names\":[],\"interests\":[],\"services\":[],\"purchases\":[],\"intent\":[],\"topics\":[]}'",
1739
+ ' signals=\'{"names":[],"interests":[],"services":[],"purchases":[],"intent":[],"topics":[]}\'',
2049
1740
  BT,
2050
1741
  "",
2051
1742
  "### 3. Daily Profile Synthesis (You Do This — Use Opus 4.6)",
@@ -2093,7 +1784,7 @@ module.exports = {
2093
1784
  "",
2094
1785
  "- **Be smart about noise**: Ignore login pages, error pages, redirects",
2095
1786
  "- **Respect privacy**: Don't store passwords, tokens, or PII like SSNs/credit cards",
2096
- "- **Extract signal from noise**: A visit to \"Nike Air Max\" tells you about shopping interest, not just a URL",
1787
+ '- **Extract signal from noise**: A visit to "Nike Air Max" tells you about shopping interest, not just a URL',
2097
1788
  "- **Cross-reference**: If user visits Stripe docs AND Vercel, they're likely a developer building a SaaS",
2098
1789
  "- **Temporal awareness**: Morning habits vs evening habits, weekday vs weekend",
2099
1790
  "- **Don't hallucinate**: Only include signals backed by actual browsing data",
@@ -2102,7 +1793,7 @@ module.exports = {
2102
1793
  "",
2103
1794
  "- **Summarization**: Every 10 minutes while the user is actively browsing",
2104
1795
  "- **Profile synthesis**: Once daily, preferably at end of day",
2105
- "- **On demand**: User can ask \"update my profile\" or \"what do you know about me\"",
1796
+ '- **On demand**: User can ask "update my profile" or "what do you know about me"',
2106
1797
  "",
2107
1798
  ].join("\n");
2108
1799
  mkdirSync(profilerSkillDir, { recursive: true });
@@ -2139,6 +1830,19 @@ module.exports = {
2139
1830
  }
2140
1831
  const npxForPlist = join(dirname(nodeForPlist), "npx");
2141
1832
 
1833
+ // Build ProgramArguments — include named tunnel flags if set
1834
+ let tunnelPlistArgs = "";
1835
+ if (tunnelToken) {
1836
+ tunnelPlistArgs += `
1837
+ <string>--tunnel-token</string>
1838
+ <string>${tunnelToken}</string>`;
1839
+ }
1840
+ if (tunnelHostname) {
1841
+ tunnelPlistArgs += `
1842
+ <string>--tunnel-hostname</string>
1843
+ <string>${tunnelHostname}</string>`;
1844
+ }
1845
+
2142
1846
  const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
2143
1847
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2144
1848
  <plist version="1.0">
@@ -2151,7 +1855,7 @@ module.exports = {
2151
1855
  <string>openclaw-navigator@latest</string>
2152
1856
  <string>--mcp</string>
2153
1857
  <string>--port</string>
2154
- <string>${port}</string>
1858
+ <string>${port}</string>${tunnelPlistArgs}
2155
1859
  </array>
2156
1860
  <key>EnvironmentVariables</key>
2157
1861
  <dict>
@@ -2203,10 +1907,6 @@ module.exports = {
2203
1907
  // ── Graceful shutdown ─────────────────────────────────────────────────
2204
1908
  const shutdown = () => {
2205
1909
  console.log(`\n${DIM}Shutting down bridge...${RESET}`);
2206
- if (uiProcess) {
2207
- uiProcess.kill();
2208
- uiProcess = null;
2209
- }
2210
1910
  if (mcpProcess) {
2211
1911
  mcpProcess.kill();
2212
1912
  }
package/mcp.mjs CHANGED
@@ -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
 
@@ -600,35 +600,50 @@ const TOOLS = [
600
600
  // ── Chat Tools ──
601
601
  {
602
602
  name: "navigator_get_chat_messages",
603
- description: "Get recent chat messages from the Navigator chat pane. Use this to see what the user is asking.",
603
+ description:
604
+ "Get recent chat messages from the Navigator chat pane. Use this to see what the user is asking.",
604
605
  inputSchema: {
605
606
  type: "object",
606
607
  properties: {
607
- sessionKey: { type: "string", description: "Chat session key (default: main)", default: "main" },
608
+ sessionKey: {
609
+ type: "string",
610
+ description: "Chat session key (default: main)",
611
+ default: "main",
612
+ },
608
613
  limit: { type: "number", description: "Max messages to return (default: 20)", default: 20 },
609
614
  },
610
615
  },
611
616
  },
612
617
  {
613
618
  name: "navigator_chat_respond",
614
- description: "Send a response message to the Navigator chat pane. The user will see this as an agent message.",
619
+ description:
620
+ "Send a response message to the Navigator chat pane. The user will see this as an agent message.",
615
621
  inputSchema: {
616
622
  type: "object",
617
623
  properties: {
618
624
  message: { type: "string", description: "The response message to send" },
619
- sessionKey: { type: "string", description: "Chat session key (default: main)", default: "main" },
625
+ sessionKey: {
626
+ type: "string",
627
+ description: "Chat session key (default: main)",
628
+ default: "main",
629
+ },
620
630
  },
621
631
  required: ["message"],
622
632
  },
623
633
  },
624
634
  {
625
635
  name: "navigator_chat_stream",
626
- description: "Stream a partial response to the Navigator chat pane (for real-time typing effect).",
636
+ description:
637
+ "Stream a partial response to the Navigator chat pane (for real-time typing effect).",
627
638
  inputSchema: {
628
639
  type: "object",
629
640
  properties: {
630
641
  text: { type: "string", description: "Partial text to stream" },
631
- sessionKey: { type: "string", description: "Chat session key (default: main)", default: "main" },
642
+ sessionKey: {
643
+ type: "string",
644
+ description: "Chat session key (default: main)",
645
+ default: "main",
646
+ },
632
647
  runId: { type: "string", description: "Run ID for grouping streamed chunks" },
633
648
  },
634
649
  required: ["text"],
@@ -758,9 +773,7 @@ async function handleTool(name, args) {
758
773
 
759
774
  // ── AI Browser Intelligence ──
760
775
  case "navigator_analyze_page":
761
- return jsonResult(
762
- await sendCommand("ai.analyze", {}, { commandName: "ai.analyze" }),
763
- );
776
+ return jsonResult(await sendCommand("ai.analyze", {}, { commandName: "ai.analyze" }));
764
777
 
765
778
  case "navigator_find_element":
766
779
  return jsonResult(
@@ -768,9 +781,7 @@ async function handleTool(name, args) {
768
781
  );
769
782
 
770
783
  case "navigator_is_ready":
771
- return jsonResult(
772
- await sendCommand("ai.ready", {}, { commandName: "ai.ready" }),
773
- );
784
+ return jsonResult(await sendCommand("ai.ready", {}, { commandName: "ai.ready" }));
774
785
 
775
786
  case "navigator_wait_for_element":
776
787
  return jsonResult(
@@ -792,36 +803,22 @@ async function handleTool(name, args) {
792
803
 
793
804
  case "navigator_smart_fill":
794
805
  return jsonResult(
795
- await sendCommand(
796
- "ai.fill",
797
- { data: args.data },
798
- { commandName: "ai.fill" },
799
- ),
806
+ await sendCommand("ai.fill", { data: args.data }, { commandName: "ai.fill" }),
800
807
  );
801
808
 
802
809
  case "navigator_intercept_api":
803
- return jsonResult(
804
- await sendCommand("ai.intercept", {}, { commandName: "ai.intercept" }),
805
- );
810
+ return jsonResult(await sendCommand("ai.intercept", {}, { commandName: "ai.intercept" }));
806
811
 
807
812
  case "navigator_set_cookies":
808
813
  return jsonResult(
809
- await sendCommand(
810
- "ai.cookies",
811
- { cookies: args.cookies },
812
- { commandName: "ai.cookies" },
813
- ),
814
+ await sendCommand("ai.cookies", { cookies: args.cookies }, { commandName: "ai.cookies" }),
814
815
  );
815
816
 
816
817
  case "navigator_get_performance":
817
- return jsonResult(
818
- await sendCommand("ai.metrics", {}, { commandName: "ai.metrics" }),
819
- );
818
+ return jsonResult(await sendCommand("ai.metrics", {}, { commandName: "ai.metrics" }));
820
819
 
821
820
  case "navigator_get_page_state":
822
- return jsonResult(
823
- await sendCommand("ai.state", {}, { commandName: "ai.state" }),
824
- );
821
+ return jsonResult(await sendCommand("ai.state", {}, { commandName: "ai.state" }));
825
822
 
826
823
  // ── User Profiling Tools ──
827
824
  case "navigator_get_page_visits": {
@@ -865,7 +862,9 @@ async function handleTool(name, args) {
865
862
  case "navigator_get_chat_messages": {
866
863
  const sessionKey = args.sessionKey || "main";
867
864
  const limit = args.limit ?? 20;
868
- const data = await bridgeGet(`/api/sessions/history?sessionKey=${encodeURIComponent(sessionKey)}`);
865
+ const data = await bridgeGet(
866
+ `/api/sessions/history?sessionKey=${encodeURIComponent(sessionKey)}`,
867
+ );
869
868
  if (data.ok && Array.isArray(data.messages)) {
870
869
  return jsonResult({ ok: true, messages: data.messages.slice(-limit) });
871
870
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.1.0",
3
+ "version": "5.2.1",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",