openclaw-navigator 4.3.3 → 4.3.5
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 +154 -46
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
18
|
import { existsSync } from "node:fs";
|
|
19
|
-
import { createServer } from "node:http";
|
|
19
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
20
20
|
import { networkInterfaces, hostname, userInfo } from "node:os";
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
@@ -55,6 +55,9 @@ const recentEvents = [];
|
|
|
55
55
|
const MAX_EVENTS = 200;
|
|
56
56
|
const validTokens = new Set();
|
|
57
57
|
|
|
58
|
+
// OC Web UI reverse proxy target (configurable via --ui-port or env)
|
|
59
|
+
let ocUIPort = parseInt(process.env.OPENCLAW_UI_PORT ?? "4000", 10);
|
|
60
|
+
|
|
58
61
|
// Pairing code state
|
|
59
62
|
let pairingCode = null;
|
|
60
63
|
let pairingData = null;
|
|
@@ -443,8 +446,84 @@ function handleRequest(req, res) {
|
|
|
443
446
|
}
|
|
444
447
|
|
|
445
448
|
// ── Health check ──
|
|
446
|
-
if (req.method === "GET" &&
|
|
447
|
-
sendJSON(res, 200, { ok: true, service: "openclaw-navigator-bridge", version: "4.
|
|
449
|
+
if (req.method === "GET" && path === "/health") {
|
|
450
|
+
sendJSON(res, 200, { ok: true, service: "openclaw-navigator-bridge", version: "4.3.4" });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Reverse proxy: /ui/* → OC Web UI (localhost:ocUIPort) ──────────────
|
|
455
|
+
// Strips /ui prefix so /ui/dashboard → localhost:4000/dashboard
|
|
456
|
+
if (path === "/ui" || path.startsWith("/ui/")) {
|
|
457
|
+
const targetPath = path === "/ui" ? "/" : path.slice(3); // strip "/ui"
|
|
458
|
+
const targetURL = `${targetPath}${url.search}`;
|
|
459
|
+
|
|
460
|
+
const proxyOpts = {
|
|
461
|
+
hostname: "127.0.0.1",
|
|
462
|
+
port: ocUIPort,
|
|
463
|
+
path: targetURL,
|
|
464
|
+
method: req.method,
|
|
465
|
+
headers: {
|
|
466
|
+
...req.headers,
|
|
467
|
+
host: `127.0.0.1:${ocUIPort}`,
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
|
|
472
|
+
// Forward status + headers (add CORS for cross-origin tunnel access)
|
|
473
|
+
const headers = { ...proxyRes.headers };
|
|
474
|
+
headers["access-control-allow-origin"] = "*";
|
|
475
|
+
headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
|
|
476
|
+
headers["access-control-allow-headers"] = "Content-Type, Authorization";
|
|
477
|
+
res.writeHead(proxyRes.statusCode ?? 502, headers);
|
|
478
|
+
proxyRes.pipe(res, { end: true });
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
proxyReq.on("error", (err) => {
|
|
482
|
+
sendJSON(res, 502, {
|
|
483
|
+
ok: false,
|
|
484
|
+
error: `OC Web UI not reachable on port ${ocUIPort}`,
|
|
485
|
+
detail: err.message,
|
|
486
|
+
hint: "Make sure the OC gateway is running (openclaw gateway start)",
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Forward request body for POST/PUT
|
|
491
|
+
req.pipe(proxyReq, { end: true });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ── Root → redirect to /ui/ if OC Web UI is available ──
|
|
496
|
+
if (req.method === "GET" && path === "") {
|
|
497
|
+
// Quick probe to check if OC UI is running
|
|
498
|
+
const probe = httpRequest(
|
|
499
|
+
{ hostname: "127.0.0.1", port: ocUIPort, path: "/", method: "HEAD", timeout: 1000 },
|
|
500
|
+
(probeRes) => {
|
|
501
|
+
probeRes.resume(); // drain
|
|
502
|
+
// OC UI is running — redirect to it
|
|
503
|
+
res.writeHead(302, { Location: "/ui/" });
|
|
504
|
+
res.end();
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
probe.on("error", () => {
|
|
508
|
+
// OC UI not running — show bridge status
|
|
509
|
+
sendJSON(res, 200, {
|
|
510
|
+
ok: true,
|
|
511
|
+
service: "openclaw-navigator-bridge",
|
|
512
|
+
version: "4.3.4",
|
|
513
|
+
ui: { available: false, port: ocUIPort, hint: "Start the OC gateway to enable /ui/" },
|
|
514
|
+
navigator: { connected: bridgeState.connected },
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
probe.on("timeout", () => {
|
|
518
|
+
probe.destroy();
|
|
519
|
+
sendJSON(res, 200, {
|
|
520
|
+
ok: true,
|
|
521
|
+
service: "openclaw-navigator-bridge",
|
|
522
|
+
version: "4.3.4",
|
|
523
|
+
ui: { available: false, port: ocUIPort, hint: "OC UI timed out" },
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
probe.end();
|
|
448
527
|
return;
|
|
449
528
|
}
|
|
450
529
|
|
|
@@ -523,6 +602,9 @@ async function main() {
|
|
|
523
602
|
if (args[i] === "--mcp") {
|
|
524
603
|
withMcp = true;
|
|
525
604
|
}
|
|
605
|
+
if (args[i] === "--ui-port" && args[i + 1]) {
|
|
606
|
+
ocUIPort = parseInt(args[i + 1], 10);
|
|
607
|
+
}
|
|
526
608
|
if (args[i] === "--help" || args[i] === "-h") {
|
|
527
609
|
console.log(`
|
|
528
610
|
${BOLD}openclaw-navigator${RESET} — One-command bridge + tunnel for Navigator
|
|
@@ -536,6 +618,7 @@ ${BOLD}Usage:${RESET}
|
|
|
536
618
|
${BOLD}Options:${RESET}
|
|
537
619
|
--port <port> Bridge server port (default: 18790)
|
|
538
620
|
--mcp Also start the MCP server (stdio) for OpenClaw agent
|
|
621
|
+
--ui-port <port> OC web UI port to reverse-proxy (default: 4000)
|
|
539
622
|
--no-tunnel Skip auto-tunnel, use SSH or LAN instead
|
|
540
623
|
--bind <host> Bind address (default: 127.0.0.1)
|
|
541
624
|
--help Show this help
|
|
@@ -655,6 +738,11 @@ ${BOLD}How it works:${RESET}
|
|
|
655
738
|
info(` Token: ${token}`);
|
|
656
739
|
}
|
|
657
740
|
|
|
741
|
+
// ── Show OC Web UI access ────────────────────────────────────────────
|
|
742
|
+
const uiURL = tunnelURL ? `${tunnelURL}/ui/` : `http://localhost:${port}/ui/`;
|
|
743
|
+
console.log(` ${BOLD}OC Web UI:${RESET} ${CYAN}${uiURL}${RESET}`);
|
|
744
|
+
info(` (reverse-proxies to localhost:${ocUIPort} — make sure OC gateway is running)`);
|
|
745
|
+
|
|
658
746
|
console.log("");
|
|
659
747
|
console.log(`${BOLD}${GREEN}Bridge is running.${RESET} Waiting for Navigator to connect...`);
|
|
660
748
|
console.log(`${DIM}Press Ctrl+C to stop.${RESET}\n`);
|
|
@@ -684,53 +772,73 @@ ${BOLD}How it works:${RESET}
|
|
|
684
772
|
ok(`MCP server started (PID ${mcpProcess.pid}) — bridge: http://localhost:${port}`);
|
|
685
773
|
|
|
686
774
|
// ── Auto-register MCP server with mcporter (if available) ─────────
|
|
687
|
-
//
|
|
688
|
-
//
|
|
775
|
+
// Writes directly to ~/.mcporter/mcporter.json for reliability.
|
|
776
|
+
//
|
|
777
|
+
// KEY FIX: We resolve stable paths instead of using process.execPath
|
|
778
|
+
// which may point to a temp npx cache (/Users/x/.npm/_npx/<hash>/node)
|
|
779
|
+
// that disappears after npx exits. We find the REAL node binary and
|
|
780
|
+
// the globally-installed mcp.mjs (or realpath of the current one).
|
|
689
781
|
try {
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
"--env",
|
|
701
|
-
`NAVIGATOR_BRIDGE_URL=http://localhost:${port}`,
|
|
702
|
-
"--description",
|
|
703
|
-
"Navigator browser control (15 tools: navigate, click, fill, read, etc.)",
|
|
704
|
-
"--scope",
|
|
705
|
-
"home",
|
|
706
|
-
],
|
|
707
|
-
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
708
|
-
);
|
|
709
|
-
|
|
710
|
-
let regOut = "";
|
|
711
|
-
reg.stdout?.on("data", (d) => {
|
|
712
|
-
regOut += d.toString();
|
|
713
|
-
});
|
|
714
|
-
reg.stderr?.on("data", (d) => {
|
|
715
|
-
regOut += d.toString();
|
|
716
|
-
});
|
|
782
|
+
const { readFileSync, writeFileSync, mkdirSync, realpathSync } = await import("node:fs");
|
|
783
|
+
const { homedir } = await import("node:os");
|
|
784
|
+
|
|
785
|
+
// Resolve stable node binary (follow symlinks out of npx temp dir)
|
|
786
|
+
let stableNode;
|
|
787
|
+
try {
|
|
788
|
+
stableNode = realpathSync(process.execPath);
|
|
789
|
+
} catch {
|
|
790
|
+
stableNode = process.execPath;
|
|
791
|
+
}
|
|
717
792
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
793
|
+
// Resolve stable mcp.mjs path
|
|
794
|
+
let stableMcpPath;
|
|
795
|
+
try {
|
|
796
|
+
stableMcpPath = realpathSync(mcpPath);
|
|
797
|
+
} catch {
|
|
798
|
+
stableMcpPath = mcpPath;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// If paths are still in npx temp cache, try to find the real install
|
|
802
|
+
if (stableMcpPath.includes("_npx") || stableMcpPath.includes(".npm/_npx")) {
|
|
803
|
+
// Check if openclaw-navigator is installed globally
|
|
804
|
+
const nodeDir = dirname(stableNode);
|
|
805
|
+
const globalMcp = join(dirname(nodeDir), "lib/node_modules/openclaw-navigator/mcp.mjs");
|
|
806
|
+
if (existsSync(globalMcp)) {
|
|
807
|
+
stableMcpPath = globalMcp;
|
|
726
808
|
}
|
|
727
|
-
|
|
809
|
+
// If still temp, that's OK — mcporter will use the path we give it
|
|
810
|
+
// and the bridge running alongside keeps the MCP server alive anyway
|
|
811
|
+
}
|
|
728
812
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
813
|
+
info(` mcporter: node=${stableNode}`);
|
|
814
|
+
info(` mcporter: mcp=${stableMcpPath}`);
|
|
815
|
+
|
|
816
|
+
// Write directly to ~/.mcporter/mcporter.json
|
|
817
|
+
const mcporterDir = join(homedir(), ".mcporter");
|
|
818
|
+
const mcporterConfigPath = join(mcporterDir, "mcporter.json");
|
|
819
|
+
|
|
820
|
+
let mcporterConfig = { mcpServers: {}, imports: [] };
|
|
821
|
+
try {
|
|
822
|
+
mcporterConfig = JSON.parse(readFileSync(mcporterConfigPath, "utf8"));
|
|
823
|
+
if (!mcporterConfig.mcpServers) mcporterConfig.mcpServers = {};
|
|
824
|
+
} catch {
|
|
825
|
+
// File doesn't exist or invalid — start fresh
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Set/update the navigator entry with stable paths
|
|
829
|
+
mcporterConfig.mcpServers.navigator = {
|
|
830
|
+
command: stableNode,
|
|
831
|
+
args: [stableMcpPath],
|
|
832
|
+
env: { NAVIGATOR_BRIDGE_URL: `http://localhost:${port}` },
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
mkdirSync(mcporterDir, { recursive: true });
|
|
836
|
+
writeFileSync(mcporterConfigPath, JSON.stringify(mcporterConfig, null, 2) + "\n", "utf8");
|
|
837
|
+
ok("Registered MCP server with mcporter (15 browser tools available)");
|
|
838
|
+
info(` Config: ${mcporterConfigPath}`);
|
|
839
|
+
} catch (err) {
|
|
840
|
+
warn(`mcporter registration failed: ${err.message}`);
|
|
841
|
+
info(" You can manually configure mcporter for Navigator MCP");
|
|
734
842
|
}
|
|
735
843
|
|
|
736
844
|
console.log("");
|