Skip to main content

priv/surf-server.js

// AgentSea Surf — Node browser-automation sidecar (Playwright).
//
// Speaks newline-delimited JSON over stdin/stdout with AgentSea.Surf.Sidecar:
//   in : {"id":N,"command":"navigate"|"text"|"screenshot"|"click"|"eval","args":{...}}
//   out: {"id":N,"ok":true,"result":...} | {"id":N,"ok":false,"error":"..."}
//
// Requires Playwright:  npm install playwright && npx playwright install chromium
//
// (This is the production server. The test suite drives the sidecar against a
//  dependency-free fake instead, so this file is not exercised in CI.)

const readline = require("readline");

let browser, page;

async function ensurePage() {
  if (!page) {
    const { chromium } = require("playwright");
    browser = await chromium.launch();
    page = await browser.newPage();
  }
  return page;
}

async function handle(command, args) {
  const p = await ensurePage();
  switch (command) {
    case "navigate":
      await p.goto(args.url, { waitUntil: "domcontentloaded" });
      return { url: p.url() };
    case "text":
      return await p.innerText("body");
    case "screenshot":
      return { base64: (await p.screenshot()).toString("base64") };
    case "click":
      await p.click(args.selector);
      return { clicked: args.selector };
    case "eval":
      return await p.evaluate(args.script);
    default:
      throw new Error(`unknown command: ${command}`);
  }
}

const rl = readline.createInterface({ input: process.stdin });

rl.on("line", async (line) => {
  line = line.trim();
  if (!line) return;
  let req;
  try {
    req = JSON.parse(line);
  } catch (e) {
    return;
  }
  try {
    const result = await handle(req.command, req.args || {});
    process.stdout.write(JSON.stringify({ id: req.id, ok: true, result }) + "\n");
  } catch (e) {
    process.stdout.write(
      JSON.stringify({ id: req.id, ok: false, error: String(e.message || e) }) + "\n",
    );
  }
});

process.on("exit", () => browser && browser.close());