@gguf/claw
Version:
WhatsApp gateway CLI (Baileys web) with Pi RPC agent
1,617 lines (1,607 loc) • 62 kB
JavaScript
import { o as createSubsystemLogger } from "./entry.js";
import { t as formatCliCommand } from "./command-format-3xiXujG0.js";
import { t as CONFIG_DIR } from "./utils-PmTbZoD1.js";
import { t as runCommandWithTimeout } from "./exec-BIMFe4XS.js";
import { t as rawDataToString } from "./ws-DlSkA_BG.js";
import { execFileSync, spawn } from "node:child_process";
import path from "node:path";
import os from "node:os";
import fs from "node:fs";
import fs$1 from "node:fs/promises";
import { randomBytes } from "node:crypto";
import { createServer } from "node:http";
import WebSocket, { WebSocketServer } from "ws";
import net from "node:net";
//#region src/browser/constants.ts
const DEFAULT_OPENCLAW_BROWSER_ENABLED = true;
const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome";
const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 8e4;
const DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 1e4;
const DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH = 6;
//#endregion
//#region src/browser/extension-relay.ts
const RELAY_AUTH_HEADER = "x-openclaw-relay-token";
function headerValue(value) {
if (!value) return;
if (Array.isArray(value)) return value[0];
return value;
}
function getHeader(req, name) {
return headerValue(req.headers[name.toLowerCase()]);
}
function isLoopbackHost$1(host) {
const h = host.trim().toLowerCase();
return h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h === "[::1]" || h === "::1" || h === "[::]" || h === "::";
}
function isLoopbackAddress(ip) {
if (!ip) return false;
if (ip === "127.0.0.1") return true;
if (ip.startsWith("127.")) return true;
if (ip === "::1") return true;
if (ip.startsWith("::ffff:127.")) return true;
return false;
}
function parseBaseUrl(raw) {
const parsed = new URL(raw.trim().replace(/\/$/, ""));
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`extension relay cdpUrl must be http(s), got ${parsed.protocol}`);
const host = parsed.hostname;
const port = parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
if (!Number.isFinite(port) || port <= 0 || port > 65535) throw new Error(`extension relay cdpUrl has invalid port: ${parsed.port || "(empty)"}`);
return {
host,
port,
baseUrl: parsed.toString().replace(/\/$/, "")
};
}
function text(res, status, bodyText) {
const body = Buffer.from(bodyText);
res.write(`HTTP/1.1 ${status} ${status === 200 ? "OK" : "ERR"}\r\nContent-Type: text/plain; charset=utf-8\r
Content-Length: ${body.length}\r\nConnection: close\r
\r
`);
res.write(body);
res.end();
}
function rejectUpgrade(socket, status, bodyText) {
text(socket, status, bodyText);
try {
socket.destroy();
} catch {}
}
const serversByPort = /* @__PURE__ */ new Map();
const relayAuthByPort = /* @__PURE__ */ new Map();
function relayAuthTokenForUrl(url) {
try {
const parsed = new URL(url);
if (!isLoopbackHost$1(parsed.hostname)) return null;
const port = parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" || parsed.protocol === "wss:" ? 443 : 80;
if (!Number.isFinite(port)) return null;
return relayAuthByPort.get(port) ?? null;
} catch {
return null;
}
}
function getChromeExtensionRelayAuthHeaders(url) {
const token = relayAuthTokenForUrl(url);
if (!token) return {};
return { [RELAY_AUTH_HEADER]: token };
}
async function ensureChromeExtensionRelayServer(opts) {
const info = parseBaseUrl(opts.cdpUrl);
if (!isLoopbackHost$1(info.host)) throw new Error(`extension relay requires loopback cdpUrl host (got ${info.host})`);
const existing = serversByPort.get(info.port);
if (existing) return existing;
let extensionWs = null;
const cdpClients = /* @__PURE__ */ new Set();
const connectedTargets = /* @__PURE__ */ new Map();
const pendingExtension = /* @__PURE__ */ new Map();
let nextExtensionId = 1;
const sendToExtension = async (payload) => {
const ws = extensionWs;
if (!ws || ws.readyState !== WebSocket.OPEN) throw new Error("Chrome extension not connected");
ws.send(JSON.stringify(payload));
return await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pendingExtension.delete(payload.id);
reject(/* @__PURE__ */ new Error(`extension request timeout: ${payload.params.method}`));
}, 3e4);
pendingExtension.set(payload.id, {
resolve,
reject,
timer
});
});
};
const broadcastToCdpClients = (evt) => {
const msg = JSON.stringify(evt);
for (const ws of cdpClients) {
if (ws.readyState !== WebSocket.OPEN) continue;
ws.send(msg);
}
};
const sendResponseToCdp = (ws, res) => {
if (ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify(res));
};
const ensureTargetEventsForClient = (ws, mode) => {
for (const target of connectedTargets.values()) if (mode === "autoAttach") ws.send(JSON.stringify({
method: "Target.attachedToTarget",
params: {
sessionId: target.sessionId,
targetInfo: {
...target.targetInfo,
attached: true
},
waitingForDebugger: false
}
}));
else ws.send(JSON.stringify({
method: "Target.targetCreated",
params: { targetInfo: {
...target.targetInfo,
attached: true
} }
}));
};
const routeCdpCommand = async (cmd) => {
switch (cmd.method) {
case "Browser.getVersion": return {
protocolVersion: "1.3",
product: "Chrome/OpenClaw-Extension-Relay",
revision: "0",
userAgent: "OpenClaw-Extension-Relay",
jsVersion: "V8"
};
case "Browser.setDownloadBehavior": return {};
case "Target.setAutoAttach":
case "Target.setDiscoverTargets": return {};
case "Target.getTargets": return { targetInfos: Array.from(connectedTargets.values()).map((t) => ({
...t.targetInfo,
attached: true
})) };
case "Target.getTargetInfo": {
const params = cmd.params ?? {};
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
if (targetId) {
for (const t of connectedTargets.values()) if (t.targetId === targetId) return { targetInfo: t.targetInfo };
}
if (cmd.sessionId && connectedTargets.has(cmd.sessionId)) {
const t = connectedTargets.get(cmd.sessionId);
if (t) return { targetInfo: t.targetInfo };
}
return { targetInfo: Array.from(connectedTargets.values())[0]?.targetInfo };
}
case "Target.attachToTarget": {
const params = cmd.params ?? {};
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
if (!targetId) throw new Error("targetId required");
for (const t of connectedTargets.values()) if (t.targetId === targetId) return { sessionId: t.sessionId };
throw new Error("target not found");
}
default: return await sendToExtension({
id: nextExtensionId++,
method: "forwardCDPCommand",
params: {
method: cmd.method,
sessionId: cmd.sessionId,
params: cmd.params
}
});
}
};
const relayAuthToken = randomBytes(32).toString("base64url");
const server = createServer((req, res) => {
const path = new URL(req.url ?? "/", info.baseUrl).pathname;
if (path.startsWith("/json")) {
const token = getHeader(req, RELAY_AUTH_HEADER);
if (!token || token !== relayAuthToken) {
res.writeHead(401);
res.end("Unauthorized");
return;
}
}
if (req.method === "HEAD" && path === "/") {
res.writeHead(200);
res.end();
return;
}
if (path === "/") {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("OK");
return;
}
if (path === "/extension/status") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ connected: Boolean(extensionWs) }));
return;
}
const cdpWsUrl = `${`ws://${req.headers.host?.trim() || `${info.host}:${info.port}`}`}/cdp`;
if ((path === "/json/version" || path === "/json/version/") && (req.method === "GET" || req.method === "PUT")) {
const payload = {
Browser: "OpenClaw/extension-relay",
"Protocol-Version": "1.3"
};
if (extensionWs) payload.webSocketDebuggerUrl = cdpWsUrl;
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(payload));
return;
}
if (new Set([
"/json",
"/json/",
"/json/list",
"/json/list/"
]).has(path) && (req.method === "GET" || req.method === "PUT")) {
const list = Array.from(connectedTargets.values()).map((t) => ({
id: t.targetId,
type: t.targetInfo.type ?? "page",
title: t.targetInfo.title ?? "",
description: t.targetInfo.title ?? "",
url: t.targetInfo.url ?? "",
webSocketDebuggerUrl: cdpWsUrl,
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${cdpWsUrl.replace("ws://", "")}`
}));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(list));
return;
}
const activateMatch = path.match(/^\/json\/activate\/(.+)$/);
if (activateMatch && (req.method === "GET" || req.method === "PUT")) {
const targetId = decodeURIComponent(activateMatch[1] ?? "").trim();
if (!targetId) {
res.writeHead(400);
res.end("targetId required");
return;
}
(async () => {
try {
await sendToExtension({
id: nextExtensionId++,
method: "forwardCDPCommand",
params: {
method: "Target.activateTarget",
params: { targetId }
}
});
} catch {}
})();
res.writeHead(200);
res.end("OK");
return;
}
const closeMatch = path.match(/^\/json\/close\/(.+)$/);
if (closeMatch && (req.method === "GET" || req.method === "PUT")) {
const targetId = decodeURIComponent(closeMatch[1] ?? "").trim();
if (!targetId) {
res.writeHead(400);
res.end("targetId required");
return;
}
(async () => {
try {
await sendToExtension({
id: nextExtensionId++,
method: "forwardCDPCommand",
params: {
method: "Target.closeTarget",
params: { targetId }
}
});
} catch {}
})();
res.writeHead(200);
res.end("OK");
return;
}
res.writeHead(404);
res.end("not found");
});
const wssExtension = new WebSocketServer({ noServer: true });
const wssCdp = new WebSocketServer({ noServer: true });
server.on("upgrade", (req, socket, head) => {
const pathname = new URL(req.url ?? "/", info.baseUrl).pathname;
const remote = req.socket.remoteAddress;
if (!isLoopbackAddress(remote)) {
rejectUpgrade(socket, 403, "Forbidden");
return;
}
const origin = headerValue(req.headers.origin);
if (origin && !origin.startsWith("chrome-extension://")) {
rejectUpgrade(socket, 403, "Forbidden: invalid origin");
return;
}
if (pathname === "/extension") {
if (extensionWs) {
rejectUpgrade(socket, 409, "Extension already connected");
return;
}
wssExtension.handleUpgrade(req, socket, head, (ws) => {
wssExtension.emit("connection", ws, req);
});
return;
}
if (pathname === "/cdp") {
const token = getHeader(req, RELAY_AUTH_HEADER);
if (!token || token !== relayAuthToken) {
rejectUpgrade(socket, 401, "Unauthorized");
return;
}
if (!extensionWs) {
rejectUpgrade(socket, 503, "Extension not connected");
return;
}
wssCdp.handleUpgrade(req, socket, head, (ws) => {
wssCdp.emit("connection", ws, req);
});
return;
}
rejectUpgrade(socket, 404, "Not Found");
});
wssExtension.on("connection", (ws) => {
extensionWs = ws;
const ping = setInterval(() => {
if (ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify({ method: "ping" }));
}, 5e3);
ws.on("message", (data) => {
let parsed = null;
try {
parsed = JSON.parse(rawDataToString(data));
} catch {
return;
}
if (parsed && typeof parsed === "object" && "id" in parsed && typeof parsed.id === "number") {
const pending = pendingExtension.get(parsed.id);
if (!pending) return;
pendingExtension.delete(parsed.id);
clearTimeout(pending.timer);
if ("error" in parsed && typeof parsed.error === "string" && parsed.error.trim()) pending.reject(new Error(parsed.error));
else pending.resolve(parsed.result);
return;
}
if (parsed && typeof parsed === "object" && "method" in parsed) {
if (parsed.method === "pong") return;
if (parsed.method !== "forwardCDPEvent") return;
const evt = parsed;
const method = evt.params?.method;
const params = evt.params?.params;
const sessionId = evt.params?.sessionId;
if (!method || typeof method !== "string") return;
if (method === "Target.attachedToTarget") {
const attached = params ?? {};
if ((attached?.targetInfo?.type ?? "page") !== "page") return;
if (attached?.sessionId && attached?.targetInfo?.targetId) {
const prev = connectedTargets.get(attached.sessionId);
const nextTargetId = attached.targetInfo.targetId;
const prevTargetId = prev?.targetId;
const changedTarget = Boolean(prev && prevTargetId && prevTargetId !== nextTargetId);
connectedTargets.set(attached.sessionId, {
sessionId: attached.sessionId,
targetId: nextTargetId,
targetInfo: attached.targetInfo
});
if (changedTarget && prevTargetId) broadcastToCdpClients({
method: "Target.detachedFromTarget",
params: {
sessionId: attached.sessionId,
targetId: prevTargetId
},
sessionId: attached.sessionId
});
if (!prev || changedTarget) broadcastToCdpClients({
method,
params,
sessionId
});
return;
}
}
if (method === "Target.detachedFromTarget") {
const detached = params ?? {};
if (detached?.sessionId) connectedTargets.delete(detached.sessionId);
broadcastToCdpClients({
method,
params,
sessionId
});
return;
}
if (method === "Target.targetInfoChanged") {
const targetInfo = (params ?? {})?.targetInfo;
const targetId = targetInfo?.targetId;
if (targetId && (targetInfo?.type ?? "page") === "page") for (const [sid, target] of connectedTargets) {
if (target.targetId !== targetId) continue;
connectedTargets.set(sid, {
...target,
targetInfo: {
...target.targetInfo,
...targetInfo
}
});
}
}
broadcastToCdpClients({
method,
params,
sessionId
});
}
});
ws.on("close", () => {
clearInterval(ping);
extensionWs = null;
for (const [, pending] of pendingExtension) {
clearTimeout(pending.timer);
pending.reject(/* @__PURE__ */ new Error("extension disconnected"));
}
pendingExtension.clear();
connectedTargets.clear();
for (const client of cdpClients) try {
client.close(1011, "extension disconnected");
} catch {}
cdpClients.clear();
});
});
wssCdp.on("connection", (ws) => {
cdpClients.add(ws);
ws.on("message", async (data) => {
let cmd = null;
try {
cmd = JSON.parse(rawDataToString(data));
} catch {
return;
}
if (!cmd || typeof cmd !== "object") return;
if (typeof cmd.id !== "number" || typeof cmd.method !== "string") return;
if (!extensionWs) {
sendResponseToCdp(ws, {
id: cmd.id,
sessionId: cmd.sessionId,
error: { message: "Extension not connected" }
});
return;
}
try {
const result = await routeCdpCommand(cmd);
if (cmd.method === "Target.setAutoAttach" && !cmd.sessionId) ensureTargetEventsForClient(ws, "autoAttach");
if (cmd.method === "Target.setDiscoverTargets") {
if ((cmd.params ?? {}).discover === true) ensureTargetEventsForClient(ws, "discover");
}
if (cmd.method === "Target.attachToTarget") {
const params = cmd.params ?? {};
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
if (targetId) {
const target = Array.from(connectedTargets.values()).find((t) => t.targetId === targetId);
if (target) ws.send(JSON.stringify({
method: "Target.attachedToTarget",
params: {
sessionId: target.sessionId,
targetInfo: {
...target.targetInfo,
attached: true
},
waitingForDebugger: false
}
}));
}
}
sendResponseToCdp(ws, {
id: cmd.id,
sessionId: cmd.sessionId,
result
});
} catch (err) {
sendResponseToCdp(ws, {
id: cmd.id,
sessionId: cmd.sessionId,
error: { message: err instanceof Error ? err.message : String(err) }
});
}
});
ws.on("close", () => {
cdpClients.delete(ws);
});
});
await new Promise((resolve, reject) => {
server.listen(info.port, info.host, () => resolve());
server.once("error", reject);
});
const port = server.address()?.port ?? info.port;
const host = info.host;
const relay = {
host,
port,
baseUrl: `${new URL(info.baseUrl).protocol}//${host}:${port}`,
cdpWsUrl: `ws://${host}:${port}/cdp`,
extensionConnected: () => Boolean(extensionWs),
stop: async () => {
serversByPort.delete(port);
relayAuthByPort.delete(port);
try {
extensionWs?.close(1001, "server stopping");
} catch {}
for (const ws of cdpClients) try {
ws.close(1001, "server stopping");
} catch {}
await new Promise((resolve) => {
server.close(() => resolve());
});
wssExtension.close();
wssCdp.close();
}
};
relayAuthByPort.set(port, relayAuthToken);
serversByPort.set(port, relay);
return relay;
}
async function stopChromeExtensionRelayServer(opts) {
const info = parseBaseUrl(opts.cdpUrl);
const existing = serversByPort.get(info.port);
if (!existing) return false;
await existing.stop();
relayAuthByPort.delete(info.port);
return true;
}
//#endregion
//#region src/browser/cdp.helpers.ts
function isLoopbackHost(host) {
const h = host.trim().toLowerCase();
return h === "localhost" || h === "127.0.0.1" || h === "0.0.0.0" || h === "[::1]" || h === "::1" || h === "[::]" || h === "::";
}
function getHeadersWithAuth(url, headers = {}) {
const mergedHeaders = {
...getChromeExtensionRelayAuthHeaders(url),
...headers
};
try {
const parsed = new URL(url);
if (Object.keys(mergedHeaders).some((key) => key.toLowerCase() === "authorization")) return mergedHeaders;
if (parsed.username || parsed.password) {
const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64");
return {
...mergedHeaders,
Authorization: `Basic ${auth}`
};
}
} catch {}
return mergedHeaders;
}
function appendCdpPath(cdpUrl, path) {
const url = new URL(cdpUrl);
url.pathname = `${url.pathname.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
return url.toString();
}
function createCdpSender(ws) {
let nextId = 1;
const pending = /* @__PURE__ */ new Map();
const send = (method, params) => {
const id = nextId++;
const msg = {
id,
method,
params
};
ws.send(JSON.stringify(msg));
return new Promise((resolve, reject) => {
pending.set(id, {
resolve,
reject
});
});
};
const closeWithError = (err) => {
for (const [, p] of pending) p.reject(err);
pending.clear();
try {
ws.close();
} catch {}
};
ws.on("message", (data) => {
try {
const parsed = JSON.parse(rawDataToString(data));
if (typeof parsed.id !== "number") return;
const p = pending.get(parsed.id);
if (!p) return;
pending.delete(parsed.id);
if (parsed.error?.message) {
p.reject(new Error(parsed.error.message));
return;
}
p.resolve(parsed.result);
} catch {}
});
ws.on("close", () => {
closeWithError(/* @__PURE__ */ new Error("CDP socket closed"));
});
return {
send,
closeWithError
};
}
async function fetchJson(url, timeoutMs = 1500, init) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const headers = getHeadersWithAuth(url, init?.headers || {});
const res = await fetch(url, {
...init,
headers,
signal: ctrl.signal
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} finally {
clearTimeout(t);
}
}
async function withCdpSocket(wsUrl, fn, opts) {
const headers = getHeadersWithAuth(wsUrl, opts?.headers ?? {});
const ws = new WebSocket(wsUrl, {
handshakeTimeout: 5e3,
...Object.keys(headers).length ? { headers } : {}
});
const { send, closeWithError } = createCdpSender(ws);
await new Promise((resolve, reject) => {
ws.once("open", () => resolve());
ws.once("error", (err) => reject(err));
});
try {
return await fn(send);
} catch (err) {
closeWithError(err instanceof Error ? err : new Error(String(err)));
throw err;
} finally {
try {
ws.close();
} catch {}
}
}
//#endregion
//#region src/browser/cdp.ts
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
const ws = new URL(wsUrl);
const cdp = new URL(cdpUrl);
if (isLoopbackHost(ws.hostname) && !isLoopbackHost(cdp.hostname)) {
ws.hostname = cdp.hostname;
const cdpPort = cdp.port || (cdp.protocol === "https:" ? "443" : "80");
if (cdpPort) ws.port = cdpPort;
ws.protocol = cdp.protocol === "https:" ? "wss:" : "ws:";
}
if (cdp.protocol === "https:" && ws.protocol === "ws:") ws.protocol = "wss:";
if (!ws.username && !ws.password && (cdp.username || cdp.password)) {
ws.username = cdp.username;
ws.password = cdp.password;
}
for (const [key, value] of cdp.searchParams.entries()) if (!ws.searchParams.has(key)) ws.searchParams.append(key, value);
return ws.toString();
}
async function captureScreenshot(opts) {
return await withCdpSocket(opts.wsUrl, async (send) => {
await send("Page.enable");
let clip;
if (opts.fullPage) {
const metrics = await send("Page.getLayoutMetrics");
const size = metrics?.cssContentSize ?? metrics?.contentSize;
const width = Number(size?.width ?? 0);
const height = Number(size?.height ?? 0);
if (width > 0 && height > 0) clip = {
x: 0,
y: 0,
width,
height,
scale: 1
};
}
const format = opts.format ?? "png";
const quality = format === "jpeg" ? Math.max(0, Math.min(100, Math.round(opts.quality ?? 85))) : void 0;
const base64 = (await send("Page.captureScreenshot", {
format,
...quality !== void 0 ? { quality } : {},
fromSurface: true,
captureBeyondViewport: true,
...clip ? { clip } : {}
}))?.data;
if (!base64) throw new Error("Screenshot failed: missing data");
return Buffer.from(base64, "base64");
});
}
async function createTargetViaCdp(opts) {
const version = await fetchJson(appendCdpPath(opts.cdpUrl, "/json/version"), 1500);
const wsUrlRaw = String(version?.webSocketDebuggerUrl ?? "").trim();
const wsUrl = wsUrlRaw ? normalizeCdpWsUrl(wsUrlRaw, opts.cdpUrl) : "";
if (!wsUrl) throw new Error("CDP /json/version missing webSocketDebuggerUrl");
return await withCdpSocket(wsUrl, async (send) => {
const created = await send("Target.createTarget", { url: opts.url });
const targetId = String(created?.targetId ?? "").trim();
if (!targetId) throw new Error("CDP Target.createTarget returned no targetId");
return { targetId };
});
}
function axValue(v) {
if (!v || typeof v !== "object") return "";
const value = v.value;
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return "";
}
function formatAriaSnapshot(nodes, limit) {
const byId = /* @__PURE__ */ new Map();
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
const referenced = /* @__PURE__ */ new Set();
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
if (!root?.nodeId) return [];
const out = [];
const stack = [{
id: root.nodeId,
depth: 0
}];
while (stack.length && out.length < limit) {
const popped = stack.pop();
if (!popped) break;
const { id, depth } = popped;
const n = byId.get(id);
if (!n) continue;
const role = axValue(n.role);
const name = axValue(n.name);
const value = axValue(n.value);
const description = axValue(n.description);
const ref = `ax${out.length + 1}`;
out.push({
ref,
role: role || "unknown",
name: name || "",
...value ? { value } : {},
...description ? { description } : {},
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
depth
});
const children = (n.childIds ?? []).filter((c) => byId.has(c));
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child) stack.push({
id: child,
depth: depth + 1
});
}
}
return out;
}
async function snapshotAria(opts) {
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
return await withCdpSocket(opts.wsUrl, async (send) => {
await send("Accessibility.enable").catch(() => {});
const res = await send("Accessibility.getFullAXTree");
return { nodes: formatAriaSnapshot(Array.isArray(res?.nodes) ? res.nodes : [], limit) };
});
}
//#endregion
//#region src/infra/ports-format.ts
function classifyPortListener(listener, port) {
const raw = `${listener.commandLine ?? ""} ${listener.command ?? ""}`.trim().toLowerCase();
if (raw.includes("openclaw")) return "gateway";
if (raw.includes("ssh")) {
const portToken = String(port);
const tunnelPattern = new RegExp(`-(l|r)\\s*${portToken}\\b|-(l|r)${portToken}\\b|:${portToken}\\b`);
if (!raw || tunnelPattern.test(raw)) return "ssh";
return "ssh";
}
return "unknown";
}
function buildPortHints(listeners, port) {
if (listeners.length === 0) return [];
const kinds = new Set(listeners.map((listener) => classifyPortListener(listener, port)));
const hints = [];
if (kinds.has("gateway")) hints.push(`Gateway already running locally. Stop it (${formatCliCommand("openclaw gateway stop")}) or use a different port.`);
if (kinds.has("ssh")) hints.push("SSH tunnel already bound to this port. Close the tunnel or use a different local port in -L.");
if (kinds.has("unknown")) hints.push("Another process is listening on this port.");
if (listeners.length > 1) hints.push("Multiple listeners detected; ensure only one gateway/tunnel per port unless intentionally running isolated profiles.");
return hints;
}
function formatPortListener(listener) {
return `${listener.pid ? `pid ${listener.pid}` : "pid ?"}${listener.user ? ` ${listener.user}` : ""}: ${listener.commandLine || listener.command || "unknown"}${listener.address ? ` (${listener.address})` : ""}`;
}
function formatPortDiagnostics(diagnostics) {
if (diagnostics.status !== "busy") return [`Port ${diagnostics.port} is free.`];
const lines = [`Port ${diagnostics.port} is already in use.`];
for (const listener of diagnostics.listeners) lines.push(`- ${formatPortListener(listener)}`);
for (const hint of diagnostics.hints) lines.push(`- ${hint}`);
return lines;
}
//#endregion
//#region src/infra/ports-lsof.ts
const LSOF_CANDIDATES = process.platform === "darwin" ? ["/usr/sbin/lsof", "/usr/bin/lsof"] : ["/usr/bin/lsof", "/usr/sbin/lsof"];
async function canExecute(path) {
try {
await fs$1.access(path, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
async function resolveLsofCommand() {
for (const candidate of LSOF_CANDIDATES) if (await canExecute(candidate)) return candidate;
return "lsof";
}
function resolveLsofCommandSync() {
for (const candidate of LSOF_CANDIDATES) try {
fs.accessSync(candidate, fs.constants.X_OK);
return candidate;
} catch {}
return "lsof";
}
//#endregion
//#region src/infra/ports-inspect.ts
function isErrno$1(err) {
return Boolean(err && typeof err === "object" && "code" in err);
}
async function runCommandSafe(argv, timeoutMs = 5e3) {
try {
const res = await runCommandWithTimeout(argv, { timeoutMs });
return {
stdout: res.stdout,
stderr: res.stderr,
code: res.code ?? 1
};
} catch (err) {
return {
stdout: "",
stderr: "",
code: 1,
error: String(err)
};
}
}
function parseLsofFieldOutput(output) {
const lines = output.split(/\r?\n/).filter(Boolean);
const listeners = [];
let current = {};
for (const line of lines) if (line.startsWith("p")) {
if (current.pid || current.command) listeners.push(current);
const pid = Number.parseInt(line.slice(1), 10);
current = Number.isFinite(pid) ? { pid } : {};
} else if (line.startsWith("c")) current.command = line.slice(1);
else if (line.startsWith("n")) {
if (!current.address) current.address = line.slice(1);
}
if (current.pid || current.command) listeners.push(current);
return listeners;
}
async function resolveUnixCommandLine(pid) {
const res = await runCommandSafe([
"ps",
"-p",
String(pid),
"-o",
"command="
]);
if (res.code !== 0) return;
return res.stdout.trim() || void 0;
}
async function resolveUnixUser(pid) {
const res = await runCommandSafe([
"ps",
"-p",
String(pid),
"-o",
"user="
]);
if (res.code !== 0) return;
return res.stdout.trim() || void 0;
}
async function readUnixListeners(port) {
const errors = [];
const res = await runCommandSafe([
await resolveLsofCommand(),
"-nP",
`-iTCP:${port}`,
"-sTCP:LISTEN",
"-FpFcn"
]);
if (res.code === 0) {
const listeners = parseLsofFieldOutput(res.stdout);
await Promise.all(listeners.map(async (listener) => {
if (!listener.pid) return;
const [commandLine, user] = await Promise.all([resolveUnixCommandLine(listener.pid), resolveUnixUser(listener.pid)]);
if (commandLine) listener.commandLine = commandLine;
if (user) listener.user = user;
}));
return {
listeners,
detail: res.stdout.trim() || void 0,
errors
};
}
const stderr = res.stderr.trim();
if (res.code === 1 && !res.error && !stderr) return {
listeners: [],
detail: void 0,
errors
};
if (res.error) errors.push(res.error);
const detail = [stderr, res.stdout.trim()].filter(Boolean).join("\n");
if (detail) errors.push(detail);
return {
listeners: [],
detail: void 0,
errors
};
}
function parseNetstatListeners(output, port) {
const listeners = [];
const portToken = `:${port}`;
for (const rawLine of output.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
if (!line.toLowerCase().includes("listen")) continue;
if (!line.includes(portToken)) continue;
const parts = line.split(/\s+/);
if (parts.length < 4) continue;
const pidRaw = parts.at(-1);
const pid = pidRaw ? Number.parseInt(pidRaw, 10) : NaN;
const localAddr = parts[1];
const listener = {};
if (Number.isFinite(pid)) listener.pid = pid;
if (localAddr?.includes(portToken)) listener.address = localAddr;
listeners.push(listener);
}
return listeners;
}
async function resolveWindowsImageName(pid) {
const res = await runCommandSafe([
"tasklist",
"/FI",
`PID eq ${pid}`,
"/FO",
"LIST"
]);
if (res.code !== 0) return;
for (const rawLine of res.stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line.toLowerCase().startsWith("image name:")) continue;
return line.slice(11).trim() || void 0;
}
}
async function resolveWindowsCommandLine(pid) {
const res = await runCommandSafe([
"wmic",
"process",
"where",
`ProcessId=${pid}`,
"get",
"CommandLine",
"/value"
]);
if (res.code !== 0) return;
for (const rawLine of res.stdout.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line.toLowerCase().startsWith("commandline=")) continue;
return line.slice(12).trim() || void 0;
}
}
async function readWindowsListeners(port) {
const errors = [];
const res = await runCommandSafe([
"netstat",
"-ano",
"-p",
"tcp"
]);
if (res.code !== 0) {
if (res.error) errors.push(res.error);
const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n");
if (detail) errors.push(detail);
return {
listeners: [],
errors
};
}
const listeners = parseNetstatListeners(res.stdout, port);
await Promise.all(listeners.map(async (listener) => {
if (!listener.pid) return;
const [imageName, commandLine] = await Promise.all([resolveWindowsImageName(listener.pid), resolveWindowsCommandLine(listener.pid)]);
if (imageName) listener.command = imageName;
if (commandLine) listener.commandLine = commandLine;
}));
return {
listeners,
detail: res.stdout.trim() || void 0,
errors
};
}
async function tryListenOnHost(port, host) {
try {
await new Promise((resolve, reject) => {
const tester = net.createServer().once("error", (err) => reject(err)).once("listening", () => {
tester.close(() => resolve());
}).listen({
port,
host,
exclusive: true
});
});
return "free";
} catch (err) {
if (isErrno$1(err) && err.code === "EADDRINUSE") return "busy";
if (isErrno$1(err) && (err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT")) return "skip";
return "unknown";
}
}
async function checkPortInUse(port) {
const hosts = [
"127.0.0.1",
"0.0.0.0",
"::1",
"::"
];
let sawUnknown = false;
for (const host of hosts) {
const result = await tryListenOnHost(port, host);
if (result === "busy") return "busy";
if (result === "unknown") sawUnknown = true;
}
return sawUnknown ? "unknown" : "free";
}
async function inspectPortUsage(port) {
const errors = [];
const result = process.platform === "win32" ? await readWindowsListeners(port) : await readUnixListeners(port);
errors.push(...result.errors);
let listeners = result.listeners;
let status = listeners.length > 0 ? "busy" : "unknown";
if (listeners.length === 0) status = await checkPortInUse(port);
if (status !== "busy") listeners = [];
const hints = buildPortHints(listeners, port);
if (status === "busy" && listeners.length === 0) hints.push("Port is in use but process details are unavailable (install lsof or run as an admin user).");
return {
port,
status,
listeners,
hints,
detail: result.detail,
errors: errors.length > 0 ? errors : void 0
};
}
//#endregion
//#region src/infra/ports.ts
var PortInUseError = class extends Error {
constructor(port, details) {
super(`Port ${port} is already in use.`);
this.name = "PortInUseError";
this.port = port;
this.details = details;
}
};
function isErrno(err) {
return Boolean(err && typeof err === "object" && "code" in err);
}
async function describePortOwner(port) {
const diagnostics = await inspectPortUsage(port);
if (diagnostics.listeners.length === 0) return;
return formatPortDiagnostics(diagnostics).join("\n");
}
async function ensurePortAvailable(port) {
try {
await new Promise((resolve, reject) => {
const tester = net.createServer().once("error", (err) => reject(err)).once("listening", () => {
tester.close(() => resolve());
}).listen(port);
});
} catch (err) {
if (isErrno(err) && err.code === "EADDRINUSE") throw new PortInUseError(port, await describePortOwner(port));
throw err;
}
}
//#endregion
//#region src/browser/chrome.executables.ts
const CHROMIUM_BUNDLE_IDS = new Set([
"com.google.Chrome",
"com.google.Chrome.beta",
"com.google.Chrome.canary",
"com.google.Chrome.dev",
"com.brave.Browser",
"com.brave.Browser.beta",
"com.brave.Browser.nightly",
"com.microsoft.Edge",
"com.microsoft.EdgeBeta",
"com.microsoft.EdgeDev",
"com.microsoft.EdgeCanary",
"org.chromium.Chromium",
"com.vivaldi.Vivaldi",
"com.operasoftware.Opera",
"com.operasoftware.OperaGX",
"com.yandex.desktop.yandex-browser",
"company.thebrowser.Browser"
]);
const CHROMIUM_DESKTOP_IDS = new Set([
"google-chrome.desktop",
"google-chrome-beta.desktop",
"google-chrome-unstable.desktop",
"brave-browser.desktop",
"microsoft-edge.desktop",
"microsoft-edge-beta.desktop",
"microsoft-edge-dev.desktop",
"microsoft-edge-canary.desktop",
"chromium.desktop",
"chromium-browser.desktop",
"vivaldi.desktop",
"vivaldi-stable.desktop",
"opera.desktop",
"opera-gx.desktop",
"yandex-browser.desktop",
"org.chromium.Chromium.desktop"
]);
const CHROMIUM_EXE_NAMES = new Set([
"chrome.exe",
"msedge.exe",
"brave.exe",
"brave-browser.exe",
"chromium.exe",
"vivaldi.exe",
"opera.exe",
"launcher.exe",
"yandex.exe",
"yandexbrowser.exe",
"google chrome",
"google chrome canary",
"brave browser",
"microsoft edge",
"chromium",
"chrome",
"brave",
"msedge",
"brave-browser",
"google-chrome",
"google-chrome-stable",
"google-chrome-beta",
"google-chrome-unstable",
"microsoft-edge",
"microsoft-edge-beta",
"microsoft-edge-dev",
"microsoft-edge-canary",
"chromium-browser",
"vivaldi",
"vivaldi-stable",
"opera",
"opera-stable",
"opera-gx",
"yandex-browser"
]);
function exists$1(filePath) {
try {
return fs.existsSync(filePath);
} catch {
return false;
}
}
function execText(command, args, timeoutMs = 1200, maxBuffer = 1024 * 1024) {
try {
const output = execFileSync(command, args, {
timeout: timeoutMs,
encoding: "utf8",
maxBuffer
});
return String(output ?? "").trim() || null;
} catch {
return null;
}
}
function inferKindFromIdentifier(identifier) {
const id = identifier.toLowerCase();
if (id.includes("brave")) return "brave";
if (id.includes("edge")) return "edge";
if (id.includes("chromium")) return "chromium";
if (id.includes("canary")) return "canary";
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser")) return "chromium";
return "chrome";
}
function inferKindFromExecutableName(name) {
const lower = name.toLowerCase();
if (lower.includes("brave")) return "brave";
if (lower.includes("edge") || lower.includes("msedge")) return "edge";
if (lower.includes("chromium")) return "chromium";
if (lower.includes("canary") || lower.includes("sxs")) return "canary";
if (lower.includes("opera") || lower.includes("vivaldi") || lower.includes("yandex")) return "chromium";
return "chrome";
}
function detectDefaultChromiumExecutable(platform) {
if (platform === "darwin") return detectDefaultChromiumExecutableMac();
if (platform === "linux") return detectDefaultChromiumExecutableLinux();
if (platform === "win32") return detectDefaultChromiumExecutableWindows();
return null;
}
function detectDefaultChromiumExecutableMac() {
const bundleId = detectDefaultBrowserBundleIdMac();
if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
if (!appPathRaw) return null;
const appPath = appPathRaw.trim().replace(/\/$/, "");
const exeName = execText("/usr/bin/defaults", [
"read",
path.join(appPath, "Contents", "Info"),
"CFBundleExecutable"
]);
if (!exeName) return null;
const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim());
if (!exists$1(exePath)) return null;
return {
kind: inferKindFromIdentifier(bundleId),
path: exePath
};
}
function detectDefaultBrowserBundleIdMac() {
const plistPath = path.join(os.homedir(), "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist");
if (!exists$1(plistPath)) return null;
const handlersRaw = execText("/usr/bin/plutil", [
"-extract",
"LSHandlers",
"json",
"-o",
"-",
"--",
plistPath
], 2e3, 5 * 1024 * 1024);
if (!handlersRaw) return null;
let handlers;
try {
handlers = JSON.parse(handlersRaw);
} catch {
return null;
}
if (!Array.isArray(handlers)) return null;
const resolveScheme = (scheme) => {
let candidate = null;
for (const entry of handlers) {
if (!entry || typeof entry !== "object") continue;
const record = entry;
if (record.LSHandlerURLScheme !== scheme) continue;
const role = typeof record.LSHandlerRoleAll === "string" && record.LSHandlerRoleAll || typeof record.LSHandlerRoleViewer === "string" && record.LSHandlerRoleViewer || null;
if (role) candidate = role;
}
return candidate;
};
return resolveScheme("http") ?? resolveScheme("https");
}
function detectDefaultChromiumExecutableLinux() {
const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) || execText("xdg-mime", [
"query",
"default",
"x-scheme-handler/http"
]);
if (!desktopId) return null;
const trimmed = desktopId.trim();
if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
const desktopPath = findDesktopFilePath(trimmed);
if (!desktopPath) return null;
const execLine = readDesktopExecLine(desktopPath);
if (!execLine) return null;
const command = extractExecutableFromExecLine(execLine);
if (!command) return null;
const resolved = resolveLinuxExecutablePath(command);
if (!resolved) return null;
const exeName = path.posix.basename(resolved).toLowerCase();
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
return {
kind: inferKindFromExecutableName(exeName),
path: resolved
};
}
function detectDefaultChromiumExecutableWindows() {
const progId = readWindowsProgId();
const command = (progId ? readWindowsCommandForProgId(progId) : null) || readWindowsCommandForProgId("http");
if (!command) return null;
const exePath = extractWindowsExecutablePath(expandWindowsEnvVars(command));
if (!exePath) return null;
if (!exists$1(exePath)) return null;
const exeName = path.win32.basename(exePath).toLowerCase();
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
return {
kind: inferKindFromExecutableName(exeName),
path: exePath
};
}
function findDesktopFilePath(desktopId) {
const candidates = [
path.join(os.homedir(), ".local", "share", "applications", desktopId),
path.join("/usr/local/share/applications", desktopId),
path.join("/usr/share/applications", desktopId),
path.join("/var/lib/snapd/desktop/applications", desktopId)
];
for (const candidate of candidates) if (exists$1(candidate)) return candidate;
return null;
}
function readDesktopExecLine(desktopPath) {
try {
const lines = fs.readFileSync(desktopPath, "utf8").split(/\r?\n/);
for (const line of lines) if (line.startsWith("Exec=")) return line.slice(5).trim();
} catch {}
return null;
}
function extractExecutableFromExecLine(execLine) {
const tokens = splitExecLine(execLine);
for (const token of tokens) {
if (!token) continue;
if (token === "env") continue;
if (token.includes("=") && !token.startsWith("/") && !token.includes("\\")) continue;
return token.replace(/^["']|["']$/g, "");
}
return null;
}
function splitExecLine(line) {
const tokens = [];
let current = "";
let inQuotes = false;
let quoteChar = "";
for (let i = 0; i < line.length; i += 1) {
const ch = line[i];
if ((ch === "\"" || ch === "'") && (!inQuotes || ch === quoteChar)) {
if (inQuotes) {
inQuotes = false;
quoteChar = "";
} else {
inQuotes = true;
quoteChar = ch;
}
continue;
}
if (!inQuotes && /\s/.test(ch)) {
if (current) {
tokens.push(current);
current = "";
}
continue;
}
current += ch;
}
if (current) tokens.push(current);
return tokens;
}
function resolveLinuxExecutablePath(command) {
const cleaned = command.trim().replace(/%[a-zA-Z]/g, "");
if (!cleaned) return null;
if (cleaned.startsWith("/")) return cleaned;
const resolved = execText("which", [cleaned], 800);
return resolved ? resolved.trim() : null;
}
function readWindowsProgId() {
const output = execText("reg", [
"query",
"HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
"/v",
"ProgId"
]);
if (!output) return null;
return output.match(/ProgId\s+REG_\w+\s+(.+)$/im)?.[1]?.trim() || null;
}
function readWindowsCommandForProgId(progId) {
const output = execText("reg", [
"query",
progId === "http" ? "HKCR\\http\\shell\\open\\command" : `HKCR\\${progId}\\shell\\open\\command`,
"/ve"
]);
if (!output) return null;
return output.match(/REG_\w+\s+(.+)$/im)?.[1]?.trim() || null;
}
function expandWindowsEnvVars(value) {
return value.replace(/%([^%]+)%/g, (_match, name) => {
const key = String(name ?? "").trim();
return key ? process.env[key] ?? `%${key}%` : _match;
});
}
function extractWindowsExecutablePath(command) {
const quoted = command.match(/"([^"]+\\.exe)"/i);
if (quoted?.[1]) return quoted[1];
const unquoted = command.match(/([^\\s]+\\.exe)/i);
if (unquoted?.[1]) return unquoted[1];
return null;
}
function findFirstExecutable(candidates) {
for (const candidate of candidates) if (exists$1(candidate.path)) return candidate;
return null;
}
function findChromeExecutableMac() {
return findFirstExecutable([
{
kind: "chrome",
path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
},
{
kind: "chrome",
path: path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
},
{
kind: "brave",
path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
},
{
kind: "brave",
path: path.join(os.homedir(), "Applications/Brave Browser.app/Contents/MacOS/Brave Browser")
},
{
kind: "edge",
path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
},
{
kind: "edge",
path: path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge")
},
{
kind: "chromium",
path: "/Applications/Chromium.app/Contents/MacOS/Chromium"
},
{
kind: "chromium",
path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium")
},
{
kind: "canary",
path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
},
{
kind: "canary",
path: path.join(os.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary")
}
]);
}
function findChromeExecutableLinux() {
return findFirstExecutable([
{
kind: "chrome",
path: "/usr/bin/google-chrome"
},
{
kind: "chrome",
path: "/usr/bin/google-chrome-stable"
},
{
kind: "chrome",
path: "/usr/bin/chrome"
},
{
kind: "brave",
path: "/usr/bin/brave-browser"
},
{
kind: "brave",
path: "/usr/bin/brave-browser-stable"
},
{
kind: "brave",
path: "/usr/bin/brave"
},
{
kind: "brave",
path: "/snap/bin/brave"
},
{
kind: "edge",
path: "/usr/bin/microsoft-edge"
},
{
kind: "edge",
path: "/usr/bin/microsoft-edge-stable"
},
{
kind: "chromium",
path: "/usr/bin/chromium"
},
{
kind: "chromium",
path: "/usr/bin/chromium-browser"
},
{
kind: "chromium",
path: "/snap/bin/chromium"
}
]);
}
function findChromeExecutableWindows() {
const localAppData = process.env.LOCALAPPDATA ?? "";
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
const joinWin = path.win32.join;
const candidates = [];
if (localAppData) {
candidates.push({
kind: "chrome",
path: joinWin(localAppData, "Google", "Chrome", "Application", "chrome.exe")
});
candidates.push({
kind: "brave",
path: joinWin(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
});
candidates.push({
kind: "edge",
path: joinWin(localAppData, "Microsoft", "Edge", "Application", "msedge.exe")
});
candidates.push({
kind: "chromium",
path: joinWin(localAppData, "Chromium", "Application", "chrome.exe")
});
candidates.push({
kind: "canary",
path: joinWin(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe")
});
}
candidates.push({
kind: "chrome",
path: joinWin(programFiles, "Google", "Chrome", "Application", "chrome.exe")
});
candidates.push({
kind: "chrome",
path: joinWin(programFilesX86, "Google", "Chrome", "Application", "chrome.exe")
});
candidates.push({
kind: "brave",
path: joinWin(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
});
candidates.push({
kind: "brave",
path: joinWin(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
});
candidates.push({
kind: "edge",
path: joinWin(programFiles, "Microsoft", "Edge", "Application", "msedge.exe")
});
candidates.push({
kind: "edge",
path: joinWin(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe")
});
return findFirstExecutable(candidates);
}
function resolveBrowserExecutableForPlatform(resolved, platform) {
if (resolved.executablePath) {
if (!exists$1(resolved.executablePath)) throw new Error(`browser.executablePath not found: ${resolved.executablePath}`);
return {
kind: "custom",
path: resolved.executablePath
};
}
const detected = detectDefaultChromiumExecutable(platform);
if (detected) return detected;
if (platform === "darwin") return findChromeExecutableMac();
if (platform === "linux") return findChromeExecutableLinux();
if (platform === "win32") return findChromeExecutableWindows();
return null;
}
//#endregion
//#region src/browser/chrome.profile-decoration.ts
function decoratedMarkerPath(userDataDir) {
return path.join(userDataDir, ".openclaw-profile-decorated");
}
function safeReadJson(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
return parsed;
} catch {
return null;
}
}
function safeWriteJson(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
function setDeep(obj, keys, value) {
let node = obj;
for (const key of keys.slice(0, -1)) {
const next = node[key];
if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
node = node[key];
}
node[keys[keys.length - 1] ?? ""] = value;
}
function parseHexRgbToSignedArgbInt(hex) {
const cleaned = hex.trim().replace(/^#/, "");
if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) return null;
const argbUnsigned = 255 << 24 | Number.parseInt(cleaned, 16);
return argbUnsigned > 2147483647 ? argbUnsigned - 4294967296 : argbUnsigned;
}
function isProfileDecorated(userDataDir, desiredName, desiredColorHex) {
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColorHex);
const localStatePath = path.join(userDataDir, "Local State");
const preferencesPath