UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,617 lines (1,607 loc) 62 kB
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