openclaw-navigator 5.5.1 → 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 +87 -9
  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.1
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,
@@ -1330,25 +1330,37 @@ function handleRequest(req, res) {
1330
1330
  }
1331
1331
 
1332
1332
  // ── Fallback: proxy unmatched paths → OC Web UI ──────────────────────
1333
- // Catches /login, /_next/*, /static/*, /favicon.ico, etc.
1333
+ // Catches /api/*, /login, /_next/*, /static/*, /favicon.ico, etc.
1334
1334
  // MUST apply the same cookie/redirect/forwarded-header treatment as /ui/*
1335
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.
1336
1340
  {
1337
1341
  const targetURL = `${path}${url.search}`;
1338
1342
  const incomingHost = req.headers.host || "localhost";
1339
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
+
1340
1358
  const proxyOpts = {
1341
1359
  hostname: "127.0.0.1",
1342
1360
  port: ocUIPort,
1343
1361
  path: targetURL,
1344
1362
  method: req.method,
1345
- headers: {
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
- },
1363
+ headers: proxyHeaders,
1352
1364
  };
1353
1365
 
1354
1366
  const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
@@ -1377,6 +1389,72 @@ function handleRequest(req, res) {
1377
1389
  );
1378
1390
  }
1379
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
1380
1458
  res.writeHead(proxyRes.statusCode ?? 502, headers);
1381
1459
  proxyRes.pipe(res, { end: true });
1382
1460
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.5.1",
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",