openclaw-navigator 4.5.0 → 4.7.0
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 +214 -30
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* openclaw-navigator v4.
|
|
4
|
+
* openclaw-navigator v4.1.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
|
+
* Auto-installs, builds, and starts the OC Web UI on first run.
|
|
9
10
|
*
|
|
10
11
|
* Usage:
|
|
11
12
|
* npx openclaw-navigator Auto-tunnel (default)
|
|
@@ -131,6 +132,146 @@ function writeJSON(filePath, data) {
|
|
|
131
132
|
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
// ── OC Web UI lifecycle ──────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
const UI_DIR = join(homedir(), ".openclaw", "ui");
|
|
138
|
+
const UI_REPO = "https://github.com/sandman66666/openclaw-ui.git";
|
|
139
|
+
let uiProcess = null;
|
|
140
|
+
|
|
141
|
+
async function isUIInstalled() {
|
|
142
|
+
return existsSync(join(UI_DIR, "package.json")) && existsSync(join(UI_DIR, ".next"));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function setupUI() {
|
|
146
|
+
const { execSync } = await import("node:child_process");
|
|
147
|
+
|
|
148
|
+
if (existsSync(join(UI_DIR, "package.json"))) {
|
|
149
|
+
info(" UI directory exists, reinstalling...");
|
|
150
|
+
return await buildUI();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
heading("Setting up OC Web UI (first time)");
|
|
154
|
+
info(" Cloning from GitHub...");
|
|
155
|
+
|
|
156
|
+
mkdirSync(join(homedir(), ".openclaw"), { recursive: true });
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
execSync(`git clone --depth 1 ${UI_REPO} "${UI_DIR}"`, {
|
|
160
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
161
|
+
timeout: 60000,
|
|
162
|
+
});
|
|
163
|
+
ok("Repository cloned");
|
|
164
|
+
} catch (err) {
|
|
165
|
+
fail(`Failed to clone UI repo: ${err.message}`);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return await buildUI();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function buildUI() {
|
|
173
|
+
const { execSync } = await import("node:child_process");
|
|
174
|
+
|
|
175
|
+
process.stdout.write(` ${DIM}Installing dependencies (this may take a minute)...${RESET}`);
|
|
176
|
+
try {
|
|
177
|
+
execSync("npm install --production=false", {
|
|
178
|
+
cwd: UI_DIR,
|
|
179
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
180
|
+
timeout: 120000,
|
|
181
|
+
env: { ...process.env, NODE_ENV: "development" },
|
|
182
|
+
});
|
|
183
|
+
process.stdout.write(`\r${" ".repeat(70)}\r`);
|
|
184
|
+
ok("Dependencies installed");
|
|
185
|
+
} catch (err) {
|
|
186
|
+
process.stdout.write(`\r${" ".repeat(70)}\r`);
|
|
187
|
+
fail(`npm install failed: ${err.stderr?.toString()?.slice(-200) || err.message}`);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
process.stdout.write(` ${DIM}Building web UI...${RESET}`);
|
|
192
|
+
try {
|
|
193
|
+
execSync("npx next build", {
|
|
194
|
+
cwd: UI_DIR,
|
|
195
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
196
|
+
timeout: 180000,
|
|
197
|
+
});
|
|
198
|
+
process.stdout.write(`\r${" ".repeat(70)}\r`);
|
|
199
|
+
ok("Web UI built successfully");
|
|
200
|
+
return true;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
process.stdout.write(`\r${" ".repeat(70)}\r`);
|
|
203
|
+
fail(`Build failed: ${err.stderr?.toString()?.slice(-200) || err.message}`);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function updateUI() {
|
|
209
|
+
const { execSync } = await import("node:child_process");
|
|
210
|
+
|
|
211
|
+
if (!existsSync(join(UI_DIR, ".git"))) {
|
|
212
|
+
warn("UI not installed yet — running full setup instead");
|
|
213
|
+
return await setupUI();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
heading("Updating OC Web UI");
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const result = execSync("git pull --rebase origin main", {
|
|
220
|
+
cwd: UI_DIR,
|
|
221
|
+
encoding: "utf8",
|
|
222
|
+
timeout: 30000,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (result.includes("Already up to date")) {
|
|
226
|
+
ok("Already up to date");
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
ok("Pulled latest changes");
|
|
231
|
+
return await buildUI();
|
|
232
|
+
} catch (err) {
|
|
233
|
+
fail(`Update failed: ${err.message}`);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function startUIServer(port) {
|
|
239
|
+
if (uiProcess) {
|
|
240
|
+
uiProcess.kill();
|
|
241
|
+
uiProcess = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
uiProcess = spawn("npx", ["next", "start", "-p", String(port)], {
|
|
245
|
+
cwd: UI_DIR,
|
|
246
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
247
|
+
env: { ...process.env, PORT: String(port), NODE_ENV: "production" },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
uiProcess.on("error", (err) => {
|
|
251
|
+
warn(`OC Web UI failed to start: ${err.message}`);
|
|
252
|
+
uiProcess = null;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
uiProcess.on("exit", (code) => {
|
|
256
|
+
if (code !== null && code !== 0) {
|
|
257
|
+
warn(`OC Web UI exited with code ${code}`);
|
|
258
|
+
}
|
|
259
|
+
uiProcess = null;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return new Promise((resolve) => {
|
|
263
|
+
const timer = setTimeout(() => {
|
|
264
|
+
ok(`OC Web UI starting on port ${port} (PID ${uiProcess?.pid})`);
|
|
265
|
+
resolve(true);
|
|
266
|
+
}, 1500);
|
|
267
|
+
|
|
268
|
+
uiProcess.on("exit", () => {
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
resolve(false);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
134
275
|
// Pairing code state
|
|
135
276
|
let pairingCode = null;
|
|
136
277
|
let pairingData = null;
|
|
@@ -265,6 +406,13 @@ function sendJSON(res, status, body) {
|
|
|
265
406
|
res.end(JSON.stringify(body));
|
|
266
407
|
}
|
|
267
408
|
|
|
409
|
+
function validateBridgeAuth(req) {
|
|
410
|
+
const authHeader = req.headers["authorization"];
|
|
411
|
+
if (!authHeader) return false;
|
|
412
|
+
const token = authHeader.replace(/^Bearer\s+/i, "");
|
|
413
|
+
return validTokens.has(token);
|
|
414
|
+
}
|
|
415
|
+
|
|
268
416
|
function handleRequest(req, res) {
|
|
269
417
|
// CORS preflight
|
|
270
418
|
if (req.method === "OPTIONS") {
|
|
@@ -314,6 +462,10 @@ function handleRequest(req, res) {
|
|
|
314
462
|
|
|
315
463
|
// ── GET /navigator/commands ──
|
|
316
464
|
if (req.method === "GET" && path === "/navigator/commands") {
|
|
465
|
+
if (!validateBridgeAuth(req)) {
|
|
466
|
+
sendJSON(res, 401, { ok: false, error: "unauthorized", hint: "Include Authorization: Bearer <token> header" });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
317
469
|
if (!bridgeState.connected) {
|
|
318
470
|
bridgeState.connected = true;
|
|
319
471
|
bridgeState.connectedAt = Date.now();
|
|
@@ -329,6 +481,10 @@ function handleRequest(req, res) {
|
|
|
329
481
|
|
|
330
482
|
// ── POST /navigator/events ──
|
|
331
483
|
if (req.method === "POST" && path === "/navigator/events") {
|
|
484
|
+
if (!validateBridgeAuth(req)) {
|
|
485
|
+
sendJSON(res, 401, { ok: false, error: "unauthorized", hint: "Include Authorization: Bearer <token> header" });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
332
488
|
readBody(req)
|
|
333
489
|
.then((bodyStr) => {
|
|
334
490
|
try {
|
|
@@ -412,6 +568,10 @@ function handleRequest(req, res) {
|
|
|
412
568
|
|
|
413
569
|
// ── POST /navigator/command ──
|
|
414
570
|
if (req.method === "POST" && path === "/navigator/command") {
|
|
571
|
+
if (!validateBridgeAuth(req)) {
|
|
572
|
+
sendJSON(res, 401, { ok: false, error: "unauthorized", hint: "Include Authorization: Bearer <token> header" });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
415
575
|
readBody(req)
|
|
416
576
|
.then((bodyStr) => {
|
|
417
577
|
try {
|
|
@@ -704,10 +864,9 @@ function handleRequest(req, res) {
|
|
|
704
864
|
}
|
|
705
865
|
|
|
706
866
|
// ── Reverse proxy: /ui/* → OC Web UI (localhost:ocUIPort) ──────────────
|
|
707
|
-
//
|
|
867
|
+
// Keep /ui prefix — Next.js basePath: "/ui" expects it
|
|
708
868
|
if (path === "/ui" || path.startsWith("/ui/")) {
|
|
709
|
-
const
|
|
710
|
-
const targetURL = `${targetPath}${url.search}`;
|
|
869
|
+
const targetURL = `${path}${url.search}`;
|
|
711
870
|
|
|
712
871
|
const proxyOpts = {
|
|
713
872
|
hostname: "127.0.0.1",
|
|
@@ -782,32 +941,16 @@ function handleRequest(req, res) {
|
|
|
782
941
|
return;
|
|
783
942
|
}
|
|
784
943
|
|
|
785
|
-
// ── Root →
|
|
944
|
+
// ── Root → redirect to /ui/ (Next.js basePath) or show bridge status ──
|
|
786
945
|
if (req.method === "GET" && path === "") {
|
|
787
946
|
// Quick probe to check if OC UI is running
|
|
788
947
|
const probe = httpRequest(
|
|
789
|
-
{ hostname: "127.0.0.1", port: ocUIPort, path: "/", method: "HEAD", timeout: 1000 },
|
|
948
|
+
{ hostname: "127.0.0.1", port: ocUIPort, path: "/ui/", method: "HEAD", timeout: 1000 },
|
|
790
949
|
(probeRes) => {
|
|
791
950
|
probeRes.resume(); // drain
|
|
792
|
-
// OC UI is running —
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
port: ocUIPort,
|
|
796
|
-
path: `/${url.search}`,
|
|
797
|
-
method: req.method,
|
|
798
|
-
headers: { ...req.headers, host: `127.0.0.1:${ocUIPort}` },
|
|
799
|
-
};
|
|
800
|
-
const rootProxy = httpRequest(rootProxyOpts, (rootRes) => {
|
|
801
|
-
const headers = { ...rootRes.headers };
|
|
802
|
-
headers["access-control-allow-origin"] = "*";
|
|
803
|
-
res.writeHead(rootRes.statusCode ?? 502, headers);
|
|
804
|
-
rootRes.pipe(res, { end: true });
|
|
805
|
-
});
|
|
806
|
-
rootProxy.on("error", () => {
|
|
807
|
-
res.writeHead(302, { Location: "/ui/" });
|
|
808
|
-
res.end();
|
|
809
|
-
});
|
|
810
|
-
rootProxy.end();
|
|
951
|
+
// OC UI is running — redirect to /ui/
|
|
952
|
+
res.writeHead(302, { Location: "/ui/" });
|
|
953
|
+
res.end();
|
|
811
954
|
},
|
|
812
955
|
);
|
|
813
956
|
probe.on("error", () => {
|
|
@@ -815,8 +958,8 @@ function handleRequest(req, res) {
|
|
|
815
958
|
sendJSON(res, 200, {
|
|
816
959
|
ok: true,
|
|
817
960
|
service: "openclaw-navigator-bridge",
|
|
818
|
-
version: "4.
|
|
819
|
-
ui: { available: false, port: ocUIPort, hint: "Start the OC
|
|
961
|
+
version: "4.6.0",
|
|
962
|
+
ui: { available: false, port: ocUIPort, hint: "Start the OC Web UI to enable /ui/" },
|
|
820
963
|
gateway: { port: ocGatewayPort },
|
|
821
964
|
navigator: { connected: bridgeState.connected },
|
|
822
965
|
});
|
|
@@ -826,7 +969,7 @@ function handleRequest(req, res) {
|
|
|
826
969
|
sendJSON(res, 200, {
|
|
827
970
|
ok: true,
|
|
828
971
|
service: "openclaw-navigator-bridge",
|
|
829
|
-
version: "4.
|
|
972
|
+
version: "4.6.0",
|
|
830
973
|
ui: { available: false, port: ocUIPort, hint: "OC UI timed out" },
|
|
831
974
|
});
|
|
832
975
|
});
|
|
@@ -931,6 +1074,9 @@ async function main() {
|
|
|
931
1074
|
let tunnelToken = null; // For named tunnels (Cloudflare)
|
|
932
1075
|
let tunnelHostname = null; // For named tunnels (stable URL)
|
|
933
1076
|
let freshIdentity = false; // --new-code: force new pairing code
|
|
1077
|
+
let setupUIFlag = false;
|
|
1078
|
+
let updateUIFlag = false;
|
|
1079
|
+
let noUIFlag = false;
|
|
934
1080
|
|
|
935
1081
|
for (let i = 0; i < args.length; i++) {
|
|
936
1082
|
if (args[i] === "--port" && args[i + 1]) {
|
|
@@ -963,6 +1109,15 @@ async function main() {
|
|
|
963
1109
|
if (args[i] === "--new-code") {
|
|
964
1110
|
freshIdentity = true;
|
|
965
1111
|
}
|
|
1112
|
+
if (args[i] === "--setup-ui") {
|
|
1113
|
+
setupUIFlag = true;
|
|
1114
|
+
}
|
|
1115
|
+
if (args[i] === "--update-ui") {
|
|
1116
|
+
updateUIFlag = true;
|
|
1117
|
+
}
|
|
1118
|
+
if (args[i] === "--no-ui") {
|
|
1119
|
+
noUIFlag = true;
|
|
1120
|
+
}
|
|
966
1121
|
if (args[i] === "--help" || args[i] === "-h") {
|
|
967
1122
|
console.log(`
|
|
968
1123
|
${BOLD}openclaw-navigator${RESET} — One-command bridge + tunnel for Navigator
|
|
@@ -981,6 +1136,9 @@ ${BOLD}Options:${RESET}
|
|
|
981
1136
|
--no-tunnel Skip auto-tunnel, use SSH or LAN instead
|
|
982
1137
|
--bind <host> Bind address (default: 127.0.0.1)
|
|
983
1138
|
--new-code Force a new pairing code (discard saved identity)
|
|
1139
|
+
--setup-ui Force (re)install + build OC Web UI
|
|
1140
|
+
--update-ui Pull latest UI changes and rebuild
|
|
1141
|
+
--no-ui Don't auto-start the web UI
|
|
984
1142
|
--help Show this help
|
|
985
1143
|
|
|
986
1144
|
${BOLD}Stability (recommended for production):${RESET}
|
|
@@ -1025,7 +1183,7 @@ module.exports = {
|
|
|
1025
1183
|
apps: [{
|
|
1026
1184
|
name: "openclaw-navigator",
|
|
1027
1185
|
script: "${npxPath}",
|
|
1028
|
-
args: "openclaw-navigator@latest --mcp --port ${port}",
|
|
1186
|
+
args: "openclaw-navigator@latest --mcp --no-ui --port ${port}",
|
|
1029
1187
|
cwd: "${homedir()}",
|
|
1030
1188
|
autorestart: true,
|
|
1031
1189
|
max_restarts: 50,
|
|
@@ -1279,6 +1437,28 @@ module.exports = {
|
|
|
1279
1437
|
console.log("");
|
|
1280
1438
|
}
|
|
1281
1439
|
|
|
1440
|
+
// ── OC Web UI: auto-setup + start ─────────────────────────────────────
|
|
1441
|
+
if (!noUIFlag) {
|
|
1442
|
+
if (setupUIFlag) {
|
|
1443
|
+
await setupUI();
|
|
1444
|
+
} else if (updateUIFlag) {
|
|
1445
|
+
await updateUI();
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if (await isUIInstalled()) {
|
|
1449
|
+
await startUIServer(ocUIPort);
|
|
1450
|
+
} else if (!setupUIFlag && !noUIFlag) {
|
|
1451
|
+
heading("OC Web UI not found — setting up automatically");
|
|
1452
|
+
const setupOk = await setupUI();
|
|
1453
|
+
if (setupOk) {
|
|
1454
|
+
await startUIServer(ocUIPort);
|
|
1455
|
+
} else {
|
|
1456
|
+
warn("Web UI setup failed — you can retry with: npx openclaw-navigator --setup-ui");
|
|
1457
|
+
warn("The bridge will still work, but /ui/* won't serve the dashboard");
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1282
1462
|
// ── Step 4: Register initial pairing code with relay ────────────────
|
|
1283
1463
|
if (tunnelURL && !tunnelToken) {
|
|
1284
1464
|
// Already registered inside startOrReconnectTunnel()
|
|
@@ -1340,7 +1520,7 @@ module.exports = {
|
|
|
1340
1520
|
};
|
|
1341
1521
|
|
|
1342
1522
|
await Promise.all([
|
|
1343
|
-
checkPort("OC Web UI", ocUIPort, "/"),
|
|
1523
|
+
checkPort("OC Web UI", ocUIPort, "/ui/"),
|
|
1344
1524
|
checkPort("OC Gateway", ocGatewayPort, "/health"),
|
|
1345
1525
|
]);
|
|
1346
1526
|
|
|
@@ -1763,6 +1943,10 @@ module.exports = {
|
|
|
1763
1943
|
// ── Graceful shutdown ─────────────────────────────────────────────────
|
|
1764
1944
|
const shutdown = () => {
|
|
1765
1945
|
console.log(`\n${DIM}Shutting down bridge...${RESET}`);
|
|
1946
|
+
if (uiProcess) {
|
|
1947
|
+
uiProcess.kill();
|
|
1948
|
+
uiProcess = null;
|
|
1949
|
+
}
|
|
1766
1950
|
if (mcpProcess) {
|
|
1767
1951
|
mcpProcess.kill();
|
|
1768
1952
|
}
|