#!/usr/bin/env node import { mkdir, writeFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import { spawn } from "node:child_process"; import path from "node:path"; function writeJson(payload) { process.stdout.write(`${JSON.stringify(payload)}\n`); } async function readStdin() { const chunks = []; for await (const chunk of process.stdin) { chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8")); } return chunks.join("").trim(); } function normalizePayload(raw) { try { const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return { ok: false, error: "INVALID_BROWSER_CONTROL_PAYLOAD: expected object", }; } return { ok: true, payload: parsed, }; } catch { return { ok: false, error: "INVALID_BROWSER_CONTROL_PAYLOAD: invalid json", }; } } function extractTargetUrl(objective) { const match = String(objective || "").match(/https?:\/\/[^\s,。;、))]+/i); return match?.[0] || undefined; } async function writeArtifact(payload) { const artifactDir = String(process.env.BOSS_CONTROL_ARTIFACT_DIR || "").trim(); if (!artifactDir) { return []; } await mkdir(artifactDir, { recursive: true }); const requestId = typeof payload.requestId === "string" && payload.requestId.trim() ? payload.requestId.trim() : `browser-${Date.now()}`; const artifactPath = path.join(artifactDir, `${requestId}.json`); await writeFile(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); return [ { kind: "json", path: artifactPath, }, ]; } function parseArgs(value) { return String(value || "") .trim() .split(/\s+/) .filter(Boolean); } function parseArgsJson(value) { const raw = String(value || "").trim(); if (!raw) { return undefined; } try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed.map((item) => String(item)).filter(Boolean) : undefined; } catch { return undefined; } } function getBrowserAutomationMode() { const raw = String(process.env.BOSS_BROWSER_AUTOMATION_MODE || "").trim().toLowerCase(); if (raw === "off" || raw === "fetch" || raw === "playwright" || raw === "auto") { return raw; } return "auto"; } function resolveCodexHome() { return String(process.env.CODEX_HOME || "").trim() || path.join(process.env.HOME || "", ".codex"); } function resolveBundledPlaywrightCommand() { const wrapper = path.join(resolveCodexHome(), "skills", "playwright", "scripts", "playwright_cli.sh"); return existsSync(wrapper) ? wrapper : undefined; } function resolveBrowserAutomationArgs(command, commandArgs, targetUrl, requestId) { const args = [...commandArgs]; const session = String(process.env.BOSS_BROWSER_AUTOMATION_SESSION || "").trim() || String(requestId || "").trim(); if (session) { args.push("--session", session); } args.push(command, targetUrl); return args; } async function runCommand(command, args) { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk) => { stdout += chunk; }); child.stderr.on("data", (chunk) => { stderr += chunk; }); child.on("error", reject); child.on("close", (code) => { if (code !== 0) { reject(new Error(stderr.trim() || `browser open exit code ${code}`)); return; } resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); }); }); } async function runBrowserAutomation(targetUrl, requestId) { const command = String(process.env.BOSS_BROWSER_AUTOMATION_COMMAND || "").trim() || resolveBundledPlaywrightCommand(); if (!command || !targetUrl) { return undefined; } const prefixArgs = parseArgsJson(process.env.BOSS_BROWSER_AUTOMATION_ARGS_JSON) ?? parseArgs(process.env.BOSS_BROWSER_AUTOMATION_ARGS); await runCommand(command, resolveBrowserAutomationArgs("open", prefixArgs, targetUrl, requestId)); const title = await runCommand( command, resolveBrowserAutomationArgs("eval", prefixArgs, "document.title", requestId), ); return title.stdout || undefined; } async function inspectPageTitle(targetUrl) { if (!targetUrl) { return undefined; } try { const response = await fetch(targetUrl, { redirect: "follow", signal: AbortSignal.timeout(8000), }); const contentType = response.headers.get("content-type") || ""; if (!contentType.includes("text/html")) { return undefined; } const html = await response.text(); const title = html.match(/