openclaw-navigator 5.4.2 → 5.5.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 (2) hide show
  1. package/cli.mjs +173 -41
  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.4.2
4
+ * openclaw-navigator v5.5.1
5
5
  *
6
6
  * One-command bridge + tunnel for the Navigator browser.
7
7
  * Starts a local bridge, creates a Cloudflare tunnel automatically,
@@ -1116,51 +1116,183 @@ function handleRequest(req, res) {
1116
1116
  return;
1117
1117
  }
1118
1118
 
1119
- // ── Reverse proxy: /api/* → OC Gateway (localhost:ocGatewayPort) ──────────
1120
- // ALL /api/* requests go to the OC gateway EXCEPT:
1121
- // - /api/auth/* falls through to web UI (NextAuth login/session)
1122
- // - /api/sessions/respond + /api/sessions/stream handled locally above
1123
- // The OC web UI frontend expects /api/agents, /api/sessions/*, /api/chat/*,
1124
- // etc. to reach the gateway. Only /api/auth/* is a Next.js API route.
1125
- if (path.startsWith("/api/") && !path.startsWith("/api/auth/") && path !== "/api/auth") {
1126
- const targetURL = `${path}${url.search}`;
1127
-
1128
- const proxyOpts = {
1129
- hostname: "127.0.0.1",
1130
- port: ocGatewayPort,
1131
- path: targetURL,
1132
- method: req.method,
1133
- headers: {
1134
- ...req.headers,
1135
- host: `127.0.0.1:${ocGatewayPort}`,
1136
- },
1137
- };
1138
-
1139
- const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
1140
- const headers = { ...proxyRes.headers };
1141
- headers["access-control-allow-origin"] = "*";
1142
- headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
1143
- headers["access-control-allow-headers"] = "Content-Type, Authorization";
1144
- res.writeHead(proxyRes.statusCode ?? 502, headers);
1145
- proxyRes.pipe(res, { end: true });
1146
- });
1119
+ // ── Smart proxy: /api/sessions/send → OC Gateway + SSE bridge ───────────
1120
+ // This is the main chat send endpoint. The gateway may return:
1121
+ // (a) JSON: { ok, response, message, ... } pass through + broadcast
1122
+ // (b) SSE: text/event-stream with data lines — collect + broadcast + return JSON
1123
+ // Either way, the response is also broadcast via WebSocket so Navigator's
1124
+ // chat pane (OCChatService) gets it via both HTTP and WS.
1125
+ if (path === "/api/sessions/send" && req.method === "POST") {
1126
+ readBody(req)
1127
+ .then((bodyStr) => {
1128
+ const proxyOpts = {
1129
+ hostname: "127.0.0.1",
1130
+ port: ocGatewayPort,
1131
+ path: `${path}${url.search}`,
1132
+ method: "POST",
1133
+ headers: {
1134
+ ...req.headers,
1135
+ host: `127.0.0.1:${ocGatewayPort}`,
1136
+ "content-type": "application/json",
1137
+ "content-length": Buffer.byteLength(bodyStr),
1138
+ },
1139
+ };
1140
+
1141
+ const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
1142
+ const contentType = (proxyRes.headers["content-type"] || "").toLowerCase();
1143
+ const isSSE = contentType.includes("text/event-stream");
1144
+
1145
+ if (isSSE) {
1146
+ // ── SSE response: collect stream, broadcast chunks, return JSON ──
1147
+ let fullText = "";
1148
+ let buffer = "";
1149
+
1150
+ proxyRes.setEncoding("utf-8");
1151
+ proxyRes.on("data", (chunk) => {
1152
+ buffer += chunk;
1153
+ // Parse SSE lines
1154
+ const lines = buffer.split("\n");
1155
+ buffer = lines.pop() || ""; // keep incomplete line
1156
+ for (const line of lines) {
1157
+ if (line.startsWith("data: ")) {
1158
+ const raw = line.slice(6).trim();
1159
+ if (raw === "[DONE]") {
1160
+ continue;
1161
+ }
1162
+ try {
1163
+ const evt = JSON.parse(raw);
1164
+ // Extract text from common SSE shapes
1165
+ const text =
1166
+ evt.text || evt.content || evt.delta?.text || evt.delta?.content || "";
1167
+ if (text) {
1168
+ fullText += text;
1169
+ broadcastToWS({
1170
+ type: "chat.delta",
1171
+ text: fullText,
1172
+ delta: text,
1173
+ sessionKey: "main",
1174
+ timestamp: Date.now(),
1175
+ });
1176
+ }
1177
+ } catch {
1178
+ // Non-JSON SSE line — treat as plain text
1179
+ if (raw && raw !== "[DONE]") {
1180
+ fullText += raw;
1181
+ }
1182
+ }
1183
+ }
1184
+ }
1185
+ });
1186
+
1187
+ proxyRes.on("end", () => {
1188
+ // Process any remaining buffer
1189
+ if (buffer.startsWith("data: ")) {
1190
+ const raw = buffer.slice(6).trim();
1191
+ if (raw && raw !== "[DONE]") {
1192
+ try {
1193
+ const evt = JSON.parse(raw);
1194
+ fullText += evt.text || evt.content || evt.delta?.text || "";
1195
+ } catch {
1196
+ fullText += raw;
1197
+ }
1198
+ }
1199
+ }
1200
+
1201
+ if (fullText) {
1202
+ // Broadcast final via WebSocket
1203
+ broadcastToWS({
1204
+ type: "chat.final",
1205
+ text: fullText,
1206
+ content: fullText,
1207
+ sessionKey: "main",
1208
+ role: "assistant",
1209
+ timestamp: Date.now(),
1210
+ });
1211
+ console.log(` ${DIM}SSE→WS bridge: ${fullText.substring(0, 80)}...${RESET}`);
1212
+ }
1213
+
1214
+ // Return clean JSON to the caller (OCChatService)
1215
+ sendJSON(res, 200, {
1216
+ ok: true,
1217
+ response: fullText || null,
1218
+ format: "sse-bridged",
1219
+ delivered: wsClients.size,
1220
+ });
1221
+ });
1222
+
1223
+ proxyRes.on("error", () => {
1224
+ sendJSON(res, 502, { ok: false, error: "SSE stream error from gateway" });
1225
+ });
1226
+ } else {
1227
+ // ── JSON response: collect body, broadcast if it has a response, pass through ──
1228
+ const chunks = [];
1229
+ proxyRes.on("data", (c) => chunks.push(c));
1230
+ proxyRes.on("end", () => {
1231
+ const body = Buffer.concat(chunks).toString("utf-8");
1232
+ let jsonBody;
1233
+ try {
1234
+ jsonBody = JSON.parse(body);
1235
+ } catch {
1236
+ // Not JSON — pass through raw
1237
+ const headers = { ...proxyRes.headers };
1238
+ headers["access-control-allow-origin"] = "*";
1239
+ res.writeHead(proxyRes.statusCode ?? 200, headers);
1240
+ res.end(body);
1241
+ return;
1242
+ }
1243
+
1244
+ // If the gateway returned an inline response, broadcast it via WebSocket
1245
+ const inlineResponse =
1246
+ jsonBody.response || jsonBody.message || jsonBody.answer || jsonBody.text || "";
1247
+ if (inlineResponse) {
1248
+ broadcastToWS({
1249
+ type: "chat.final",
1250
+ text: inlineResponse,
1251
+ content: inlineResponse,
1252
+ sessionKey: "main",
1253
+ role: "assistant",
1254
+ timestamp: Date.now(),
1255
+ });
1256
+ console.log(
1257
+ ` ${DIM}Inline response→WS: ${inlineResponse.substring(0, 80)}...${RESET}`,
1258
+ );
1259
+ }
1260
+
1261
+ sendJSON(res, proxyRes.statusCode ?? 200, jsonBody);
1262
+ });
1263
+ }
1264
+ });
1147
1265
 
1148
- proxyReq.on("error", (err) => {
1149
- sendJSON(res, 502, {
1150
- ok: false,
1151
- error: `OC Gateway not reachable on port ${ocGatewayPort}`,
1152
- detail: err.message,
1153
- hint:
1154
- "Make sure the OC gateway is running on port " +
1155
- ocGatewayPort +
1156
- " (openclaw gateway start)",
1157
- });
1158
- });
1266
+ proxyReq.on("error", (err) => {
1267
+ sendJSON(res, 502, {
1268
+ ok: false,
1269
+ error: `OC Gateway not reachable on port ${ocGatewayPort}`,
1270
+ detail: err.message,
1271
+ });
1272
+ });
1159
1273
 
1160
- req.pipe(proxyReq, { end: true });
1274
+ proxyReq.write(bodyStr);
1275
+ proxyReq.end();
1276
+ })
1277
+ .catch(() => sendJSON(res, 400, { ok: false, error: "Bad request body" }));
1161
1278
  return;
1162
1279
  }
1163
1280
 
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).
1295
+
1164
1296
  // ── Root → redirect to /ui/ (Next.js basePath) or show bridge status ──
1165
1297
  if (req.method === "GET" && path === "") {
1166
1298
  // Quick probe to check if OC UI is running
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.4.2",
3
+ "version": "5.5.1",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",