← 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); });
}