← back to Jevrun Runner

index.mjs

72 lines

// jevrun-runner — persistent keep-alive service around jevrun's run(page, prompt).
// Holds ONE chromium browser open; creates a fresh page per task (jevrun rejects
// concurrent runs on the same page). Stays alive/healthy even without a key —
// task execution needs TYPESAFE_API_KEY, and says so plainly if it's missing.
import http from "node:http";
import { chromium } from "playwright";
import { run } from "jevrun";

const PORT = Number(process.env.JEVRUN_PORT || 9788);
const HOST = process.env.JEVRUN_HOST || "127.0.0.1";
let browser = null;
let launching = null;

async function getBrowser() {
  if (browser?.isConnected()) return browser;
  if (!launching) launching = chromium.launch({ headless: true }).then((b) => { browser = b; launching = null; return b; });
  return launching;
}

function send(res, code, obj) {
  const body = JSON.stringify(obj);
  res.writeHead(code, { "content-type": "application/json" });
  res.end(body);
}

function readJson(req) {
  return new Promise((resolve, reject) => {
    let d = "";
    req.on("data", (c) => { d += c; if (d.length > 2_000_000) req.destroy(); });
    req.on("end", () => { try { resolve(d ? JSON.parse(d) : {}); } catch (e) { reject(e); } });
    req.on("error", reject);
  });
}

const server = http.createServer(async (req, res) => {
  try {
    if (req.method === "GET" && req.url === "/health") {
      return send(res, 200, {
        status: "alive",
        browser: browser?.isConnected() ? "connected" : "idle",
        hasKey: Boolean(process.env.TYPESAFE_API_KEY),
        port: PORT,
      });
    }
    if (req.method === "POST" && req.url === "/run") {
      const { url, task, options } = await readJson(req);
      if (!process.env.TYPESAFE_API_KEY) {
        return send(res, 428, { error: "TYPESAFE_API_KEY not set — service is alive but cannot run tasks. Route the key via the `secrets` skill." });
      }
      if (!task) return send(res, 400, { error: "missing 'task' (the natural-language prompt)" });
      const b = await getBrowser();
      const page = await b.newPage();
      try {
        if (url) await page.goto(url, { waitUntil: "load" });
        const result = await run(page, task, options || {});
        return send(res, 200, { ok: true, result });
      } finally {
        await page.close().catch(() => {});
      }
    }
    return send(res, 404, { error: "not found", routes: ["GET /health", "POST /run {url?, task, options?}"] });
  } catch (e) {
    return send(res, 500, { error: String(e?.message || e) });
  }
});

server.listen(PORT, HOST, () => console.log(`jevrun-runner alive on http://${HOST}:${PORT} (key=${process.env.TYPESAFE_API_KEY ? "set" : "MISSING"})`));

for (const sig of ["SIGINT", "SIGTERM"]) {
  process.on(sig, async () => { try { await browser?.close(); } catch {} process.exit(0); });
}