openclaw-navigator 5.5.0 → 5.5.2

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 +101 -54
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * openclaw-navigator v5.5.0
4
+ * openclaw-navigator v5.5.2
5
5
  *
6
6
  * One-command bridge + tunnel for the Navigator browser.
7
7
  * Starts a local bridge, creates a Cloudflare tunnel automatically,
@@ -1278,51 +1278,20 @@ function handleRequest(req, res) {
1278
1278
  return;
1279
1279
  }
1280
1280
 
1281
- // ── Reverse proxy: /api/* OC Gateway (localhost:ocGatewayPort) ──────────
1282
- // ALL /api/* requests go to the OC gateway EXCEPT:
1283
- // - /api/auth/* falls through to web UI (NextAuth login/session)
1284
- // - /api/sessions/respond + /api/sessions/stream handled locally above
1285
- // - /api/sessions/send smart SSE bridge above
1286
- // The OC web UI frontend expects /api/agents, /api/sessions/*, /api/chat/*,
1287
- // etc. to reach the gateway. Only /api/auth/* is a Next.js API route.
1288
- if (path.startsWith("/api/") && !path.startsWith("/api/auth/") && path !== "/api/auth") {
1289
- const targetURL = `${path}${url.search}`;
1290
-
1291
- const proxyOpts = {
1292
- hostname: "127.0.0.1",
1293
- port: ocGatewayPort,
1294
- path: targetURL,
1295
- method: req.method,
1296
- headers: {
1297
- ...req.headers,
1298
- host: `127.0.0.1:${ocGatewayPort}`,
1299
- },
1300
- };
1301
-
1302
- const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
1303
- const headers = { ...proxyRes.headers };
1304
- headers["access-control-allow-origin"] = "*";
1305
- headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
1306
- headers["access-control-allow-headers"] = "Content-Type, Authorization";
1307
- res.writeHead(proxyRes.statusCode ?? 502, headers);
1308
- proxyRes.pipe(res, { end: true });
1309
- });
1310
-
1311
- proxyReq.on("error", (err) => {
1312
- sendJSON(res, 502, {
1313
- ok: false,
1314
- error: `OC Gateway not reachable on port ${ocGatewayPort}`,
1315
- detail: err.message,
1316
- hint:
1317
- "Make sure the OC gateway is running on port " +
1318
- ocGatewayPort +
1319
- " (openclaw gateway start)",
1320
- });
1321
- });
1322
-
1323
- req.pipe(proxyReq, { end: true });
1324
- return;
1325
- }
1281
+ // ── /api/* routing strategy ──────────────────────────────────────────────
1282
+ // NO direct gateway proxy for /api/* here. The web UI (Next.js on port 4000)
1283
+ // has its own /api/* routes that act as a BFF (Backend for Frontend)
1284
+ // they call the gateway internally (localhost:18789) and return clean JSON.
1285
+ // Routing /api/* directly to the gateway bypasses the BFF and breaks things
1286
+ // (e.g. /api/agents returns raw SSE instead of JSON).
1287
+ //
1288
+ // Specific bridge-handled routes (above this point):
1289
+ // - /api/sessions/respond local WebSocket broadcast
1290
+ // - /api/sessions/stream → local WebSocket broadcast
1291
+ // - /api/sessions/send smart SSE bridge to gateway (sidepane chat)
1292
+ //
1293
+ // Everything else (/api/agents, /api/auth/*, /api/chat/*, etc.) falls through
1294
+ // to the fallback proxy below, which sends it to port 4000 (web UI).
1326
1295
 
1327
1296
  // ── Root → redirect to /ui/ (Next.js basePath) or show bridge status ──
1328
1297
  if (req.method === "GET" && path === "") {
@@ -1361,25 +1330,37 @@ function handleRequest(req, res) {
1361
1330
  }
1362
1331
 
1363
1332
  // ── Fallback: proxy unmatched paths → OC Web UI ──────────────────────
1364
- // Catches /login, /_next/*, /static/*, /favicon.ico, etc.
1333
+ // Catches /api/*, /login, /_next/*, /static/*, /favicon.ico, etc.
1365
1334
  // MUST apply the same cookie/redirect/forwarded-header treatment as /ui/*
1366
1335
  // or login won't work (session cookies get lost through the tunnel).
1336
+ //
1337
+ // SSE safety: If the web UI returns text/event-stream but the browser
1338
+ // expects JSON (e.g. /api/agents), we collect the SSE events and return
1339
+ // them as a JSON array. This prevents "data: ... is not valid JSON" errors.
1367
1340
  {
1368
1341
  const targetURL = `${path}${url.search}`;
1369
1342
  const incomingHost = req.headers.host || "localhost";
1370
1343
 
1344
+ // Encourage JSON responses from the web UI's API routes
1345
+ const proxyHeaders = {
1346
+ ...req.headers,
1347
+ host: `127.0.0.1:${ocUIPort}`,
1348
+ "x-forwarded-host": incomingHost,
1349
+ "x-forwarded-proto": activeTunnelURL ? "https" : "http",
1350
+ "x-forwarded-for": req.socket.remoteAddress || "127.0.0.1",
1351
+ };
1352
+
1353
+ // For /api/* requests, prefer JSON over SSE
1354
+ if (path.startsWith("/api/")) {
1355
+ proxyHeaders["accept"] = "application/json, text/plain, */*";
1356
+ }
1357
+
1371
1358
  const proxyOpts = {
1372
1359
  hostname: "127.0.0.1",
1373
1360
  port: ocUIPort,
1374
1361
  path: targetURL,
1375
1362
  method: req.method,
1376
- headers: {
1377
- ...req.headers,
1378
- host: `127.0.0.1:${ocUIPort}`,
1379
- "x-forwarded-host": incomingHost,
1380
- "x-forwarded-proto": activeTunnelURL ? "https" : "http",
1381
- "x-forwarded-for": req.socket.remoteAddress || "127.0.0.1",
1382
- },
1363
+ headers: proxyHeaders,
1383
1364
  };
1384
1365
 
1385
1366
  const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
@@ -1408,6 +1389,72 @@ function handleRequest(req, res) {
1408
1389
  );
1409
1390
  }
1410
1391
 
1392
+ // ── SSE→JSON conversion for /api/* endpoints ─────────────────────
1393
+ // If the upstream returns text/event-stream but the request was an
1394
+ // API call (which the frontend expects as JSON), we collect the SSE
1395
+ // data events and return them as a JSON response.
1396
+ const contentType = (proxyRes.headers["content-type"] || "").toLowerCase();
1397
+ const isSSE = contentType.includes("text/event-stream");
1398
+
1399
+ if (isSSE && path.startsWith("/api/")) {
1400
+ console.log(` ${DIM}SSE→JSON: converting ${path}${RESET}`);
1401
+ let buffer = "";
1402
+ const items = [];
1403
+
1404
+ proxyRes.setEncoding("utf-8");
1405
+ proxyRes.on("data", (chunk) => {
1406
+ buffer += chunk;
1407
+ const lines = buffer.split("\n");
1408
+ buffer = lines.pop() || "";
1409
+ for (const line of lines) {
1410
+ if (line.startsWith("data: ")) {
1411
+ const raw = line.slice(6).trim();
1412
+ if (raw === "[DONE]" || !raw) {
1413
+ continue;
1414
+ }
1415
+ try {
1416
+ items.push(JSON.parse(raw));
1417
+ } catch {
1418
+ // Non-JSON SSE data — wrap as text
1419
+ if (raw) {
1420
+ items.push({ text: raw });
1421
+ }
1422
+ }
1423
+ }
1424
+ }
1425
+ });
1426
+
1427
+ proxyRes.on("end", () => {
1428
+ // Process remaining buffer
1429
+ if (buffer.startsWith("data: ")) {
1430
+ const raw = buffer.slice(6).trim();
1431
+ if (raw && raw !== "[DONE]") {
1432
+ try {
1433
+ items.push(JSON.parse(raw));
1434
+ } catch {
1435
+ if (raw) {
1436
+ items.push({ text: raw });
1437
+ }
1438
+ }
1439
+ }
1440
+ }
1441
+
1442
+ // Return as JSON — single object if 1 item, array if multiple
1443
+ const result = items.length === 1 ? items[0] : items;
1444
+ headers["content-type"] = "application/json";
1445
+ delete headers["content-length"];
1446
+ delete headers["transfer-encoding"];
1447
+ res.writeHead(proxyRes.statusCode ?? 200, headers);
1448
+ res.end(JSON.stringify(result));
1449
+ });
1450
+
1451
+ proxyRes.on("error", () => {
1452
+ sendJSON(res, 502, { ok: false, error: "SSE stream error" });
1453
+ });
1454
+ return;
1455
+ }
1456
+
1457
+ // Non-SSE responses — stream through as-is
1411
1458
  res.writeHead(proxyRes.statusCode ?? 502, headers);
1412
1459
  proxyRes.pipe(res, { end: true });
1413
1460
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.5.0",
3
+ "version": "5.5.2",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",