openclaw-navigator 5.5.3 → 5.6.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 +110 -59
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -362,6 +362,19 @@ function validateBridgeAuth(req) {
362
362
  return validTokens.has(token);
363
363
  }
364
364
 
365
+ // ── Chat session storage (in-memory) ────────────────────────────────────
366
+ // Stores messages per session key. Navigator sends user messages here,
367
+ // the OC agent reads them via navigator_get_chat_messages MCP tool,
368
+ // and pushes responses via navigator_chat_respond MCP tool.
369
+ const chatSessions = new Map();
370
+
371
+ function getChatSession(sessionKey = "main") {
372
+ if (!chatSessions.has(sessionKey)) {
373
+ chatSessions.set(sessionKey, { messages: [], createdAt: Date.now() });
374
+ }
375
+ return chatSessions.get(sessionKey);
376
+ }
377
+
365
378
  // ── WebSocket server for chat (minimal, no dependencies) ────────────────
366
379
  // Tracks connected WebSocket clients. When the OC agent pushes messages
367
380
  // via /api/sessions/respond or /api/sessions/stream, we broadcast to all
@@ -1069,6 +1082,13 @@ function handleRequest(req, res) {
1069
1082
  sendJSON(res, 400, { ok: false, error: "Missing 'content' or 'message'" });
1070
1083
  return;
1071
1084
  }
1085
+ // Store assistant message in local session
1086
+ const session = getChatSession(sessionKey);
1087
+ session.messages.push({
1088
+ role: "assistant",
1089
+ content: message,
1090
+ timestamp: Date.now(),
1091
+ });
1072
1092
  // Broadcast as a final chat message via WebSocket
1073
1093
  broadcastToWS({
1074
1094
  type: "chat.final",
@@ -1116,23 +1136,54 @@ function handleRequest(req, res) {
1116
1136
  return;
1117
1137
  }
1118
1138
 
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.
1139
+ // ── Chat send: store locally + relay to OC Gateway for processing ──────
1140
+ // HYBRID approach:
1141
+ // 1. Store user message in bridge (for MCP tools to read via history)
1142
+ // 2. Relay to OC Gateway (port 18789) so the AI agent processes it
1143
+ // 3. Gateway may return response inline (JSON/SSE) store + broadcast
1144
+ // 4. Or agent responds async via navigator_chat_respond MCP tool
1125
1145
  if (path === "/api/sessions/send" && req.method === "POST") {
1126
1146
  readBody(req)
1127
1147
  .then((bodyStr) => {
1148
+ let data;
1149
+ try {
1150
+ data = JSON.parse(bodyStr);
1151
+ } catch {
1152
+ sendJSON(res, 400, { ok: false, error: "Invalid JSON" });
1153
+ return;
1154
+ }
1155
+
1156
+ const message = data.message || data.content || data.text || "";
1157
+ const sessionKey = data.sessionKey || "main";
1158
+ if (!message) {
1159
+ sendJSON(res, 400, { ok: false, error: "Missing 'message'" });
1160
+ return;
1161
+ }
1162
+
1163
+ // 1. Store user message locally
1164
+ const session = getChatSession(sessionKey);
1165
+ session.messages.push({
1166
+ role: "user",
1167
+ content: message,
1168
+ timestamp: Date.now(),
1169
+ });
1170
+ broadcastToWS({
1171
+ type: "chat.user",
1172
+ text: message,
1173
+ content: message,
1174
+ sessionKey,
1175
+ role: "user",
1176
+ timestamp: Date.now(),
1177
+ });
1178
+ console.log(` ${DIM}Chat send [${sessionKey}]: ${message.substring(0, 60)}...${RESET}`);
1179
+
1180
+ // 2. Relay to OC Gateway for AI processing
1128
1181
  const proxyOpts = {
1129
1182
  hostname: "127.0.0.1",
1130
1183
  port: ocGatewayPort,
1131
- path: `${path}${url.search}`,
1184
+ path: `/api/sessions/send`,
1132
1185
  method: "POST",
1133
1186
  headers: {
1134
- ...req.headers,
1135
- host: `127.0.0.1:${ocGatewayPort}`,
1136
1187
  "content-type": "application/json",
1137
1188
  "content-length": Buffer.byteLength(bodyStr),
1138
1189
  },
@@ -1150,18 +1201,14 @@ function handleRequest(req, res) {
1150
1201
  proxyRes.setEncoding("utf-8");
1151
1202
  proxyRes.on("data", (chunk) => {
1152
1203
  buffer += chunk;
1153
- // Parse SSE lines
1154
1204
  const lines = buffer.split("\n");
1155
- buffer = lines.pop() || ""; // keep incomplete line
1205
+ buffer = lines.pop() || "";
1156
1206
  for (const line of lines) {
1157
1207
  if (line.startsWith("data: ")) {
1158
1208
  const raw = line.slice(6).trim();
1159
- if (raw === "[DONE]") {
1160
- continue;
1161
- }
1209
+ if (raw === "[DONE]") continue;
1162
1210
  try {
1163
1211
  const evt = JSON.parse(raw);
1164
- // Extract text from common SSE shapes
1165
1212
  const text =
1166
1213
  evt.text || evt.content || evt.delta?.text || evt.delta?.content || "";
1167
1214
  if (text) {
@@ -1170,22 +1217,18 @@ function handleRequest(req, res) {
1170
1217
  type: "chat.delta",
1171
1218
  text: fullText,
1172
1219
  delta: text,
1173
- sessionKey: "main",
1220
+ sessionKey,
1174
1221
  timestamp: Date.now(),
1175
1222
  });
1176
1223
  }
1177
1224
  } catch {
1178
- // Non-JSON SSE line treat as plain text
1179
- if (raw && raw !== "[DONE]") {
1180
- fullText += raw;
1181
- }
1225
+ if (raw && raw !== "[DONE]") fullText += raw;
1182
1226
  }
1183
1227
  }
1184
1228
  }
1185
1229
  });
1186
1230
 
1187
1231
  proxyRes.on("end", () => {
1188
- // Process any remaining buffer
1189
1232
  if (buffer.startsWith("data: ")) {
1190
1233
  const raw = buffer.slice(6).trim();
1191
1234
  if (raw && raw !== "[DONE]") {
@@ -1197,34 +1240,36 @@ function handleRequest(req, res) {
1197
1240
  }
1198
1241
  }
1199
1242
  }
1200
-
1201
1243
  if (fullText) {
1202
- // Broadcast final via WebSocket
1244
+ // Store assistant response
1245
+ session.messages.push({
1246
+ role: "assistant",
1247
+ content: fullText,
1248
+ timestamp: Date.now(),
1249
+ });
1203
1250
  broadcastToWS({
1204
1251
  type: "chat.final",
1205
1252
  text: fullText,
1206
1253
  content: fullText,
1207
- sessionKey: "main",
1254
+ sessionKey,
1208
1255
  role: "assistant",
1209
1256
  timestamp: Date.now(),
1210
1257
  });
1211
- console.log(` ${DIM}SSE→WS bridge: ${fullText.substring(0, 80)}...${RESET}`);
1258
+ console.log(` ${DIM}Gateway SSE→WS: ${fullText.substring(0, 80)}...${RESET}`);
1212
1259
  }
1213
-
1214
- // Return clean JSON to the caller (OCChatService)
1215
1260
  sendJSON(res, 200, {
1216
1261
  ok: true,
1262
+ stored: true,
1217
1263
  response: fullText || null,
1218
- format: "sse-bridged",
1219
- delivered: wsClients.size,
1264
+ source: "gateway-sse",
1220
1265
  });
1221
1266
  });
1222
1267
 
1223
1268
  proxyRes.on("error", () => {
1224
- sendJSON(res, 502, { ok: false, error: "SSE stream error from gateway" });
1269
+ sendJSON(res, 200, { ok: true, stored: true, response: null, source: "gateway-sse-error" });
1225
1270
  });
1226
1271
  } else {
1227
- // ── JSON response: collect body, broadcast if it has a response, pass through ──
1272
+ // ── JSON response from gateway ──
1228
1273
  const chunks = [];
1229
1274
  proxyRes.on("data", (c) => chunks.push(c));
1230
1275
  proxyRes.on("end", () => {
@@ -1233,42 +1278,43 @@ function handleRequest(req, res) {
1233
1278
  try {
1234
1279
  jsonBody = JSON.parse(body);
1235
1280
  } 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);
1281
+ // Non-JSON — message is stored, agent will respond via MCP
1282
+ sendJSON(res, 200, { ok: true, stored: true, response: null, source: "gateway-raw" });
1241
1283
  return;
1242
1284
  }
1243
-
1244
- // If the gateway returned an inline response, broadcast it via WebSocket
1285
+ // Check if gateway returned an inline AI response
1245
1286
  const inlineResponse =
1246
1287
  jsonBody.response || jsonBody.message || jsonBody.answer || jsonBody.text || "";
1247
1288
  if (inlineResponse) {
1289
+ session.messages.push({
1290
+ role: "assistant",
1291
+ content: inlineResponse,
1292
+ timestamp: Date.now(),
1293
+ });
1248
1294
  broadcastToWS({
1249
1295
  type: "chat.final",
1250
1296
  text: inlineResponse,
1251
1297
  content: inlineResponse,
1252
- sessionKey: "main",
1298
+ sessionKey,
1253
1299
  role: "assistant",
1254
1300
  timestamp: Date.now(),
1255
1301
  });
1256
- console.log(
1257
- ` ${DIM}Inline response→WS: ${inlineResponse.substring(0, 80)}...${RESET}`,
1258
- );
1302
+ console.log(` ${DIM}Gateway inline→WS: ${inlineResponse.substring(0, 80)}...${RESET}`);
1259
1303
  }
1260
-
1261
- sendJSON(res, proxyRes.statusCode ?? 200, jsonBody);
1304
+ sendJSON(res, 200, {
1305
+ ok: true,
1306
+ stored: true,
1307
+ response: inlineResponse || null,
1308
+ source: "gateway-json",
1309
+ });
1262
1310
  });
1263
1311
  }
1264
1312
  });
1265
1313
 
1266
1314
  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
- });
1315
+ // Gateway not reachable — message is still stored, agent can use MCP tools
1316
+ console.log(` ${DIM}Gateway not reachable (${err.message}) — message stored for MCP polling${RESET}`);
1317
+ sendJSON(res, 200, { ok: true, stored: true, response: null, source: "local-only" });
1272
1318
  });
1273
1319
 
1274
1320
  proxyReq.write(bodyStr);
@@ -1278,17 +1324,22 @@ function handleRequest(req, res) {
1278
1324
  return;
1279
1325
  }
1280
1326
 
1327
+ // ── Chat history: return stored messages for a session ────────────────
1328
+ // Called by the OC agent (via navigator_get_chat_messages MCP tool)
1329
+ // and by Navigator's OCChatService to load history on tab switch.
1330
+ if (path === "/api/sessions/history" && req.method === "GET") {
1331
+ const sessionKey = url.searchParams?.get("sessionKey") || "main";
1332
+ const session = getChatSession(sessionKey);
1333
+ sendJSON(res, 200, { ok: true, messages: session.messages });
1334
+ return;
1335
+ }
1336
+
1281
1337
  // ── /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)
1338
+ // The bridge handles chat endpoints locally:
1339
+ // - /api/sessions/send → store user message + WS broadcast
1340
+ // - /api/sessions/history → return stored messages
1341
+ // - /api/sessions/respond → store assistant message + WS broadcast
1342
+ // - /api/sessions/stream → WS broadcast (streaming delta)
1292
1343
  //
1293
1344
  // Everything else (/api/agents, /api/auth/*, /api/chat/*, etc.) falls through
1294
1345
  // to the fallback proxy below, which sends it to port 4000 (web UI).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.5.3",
3
+ "version": "5.6.1",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",