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.
- package/cli.mjs +175 -475
- package/mcp.mjs +32 -33
- 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
|
|
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
|
-
*
|
|
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
|
|
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 {
|
|
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
|
|
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)
|
|
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
|
|
467
|
-
"/
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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
|
|
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:
|
|
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;
|
|
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 (
|
|
1281
|
-
/api
|
|
1282
|
-
/ws, WebSocket →
|
|
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 {
|
|
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 --
|
|
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(
|
|
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
|
|
1145
|
+
// ── WebSocket proxy — forward /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
|
-
//
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
.
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1175
|
+
gwSocket.on("error", (err) => {
|
|
1176
|
+
console.log(` ${DIM}WS proxy: gateway unreachable — ${err.message}${RESET}`);
|
|
1177
|
+
socket.destroy();
|
|
1502
1178
|
});
|
|
1503
1179
|
|
|
1504
|
-
|
|
1505
|
-
|
|
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)
|
|
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(
|
|
1286
|
+
const delay = Math.min(
|
|
1287
|
+
2000 * Math.pow(2, tunnelReconnectAttempts),
|
|
1288
|
+
MAX_TUNNEL_RECONNECT_DELAY,
|
|
1289
|
+
);
|
|
1582
1290
|
tunnelReconnectAttempts++;
|
|
1583
|
-
warn(
|
|
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(
|
|
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
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
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/*
|
|
1718
|
-
info(` /api
|
|
1719
|
-
info(` /ws
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: {
|
|
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:
|
|
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: {
|
|
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:
|
|
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: {
|
|
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(
|
|
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
|
}
|