← back to Jevrun Runner
jevrun-runner: pm2 keep-alive service + global jevrun CLI around jevrun@0.0.2
16c1ecd0a2661ccb8738351c212b47ba220c4d4a · 2026-09-22 12:19:30 -0700 · steve
Files touched
A .gitignoreA bin/jevrun.mjsA index.mjsA package-lock.jsonA package.json
Diff
commit 16c1ecd0a2661ccb8738351c212b47ba220c4d4a
Author: steve <steve@designerwallcoverings.com>
Date: Tue Sep 22 12:19:30 2026 -0700
jevrun-runner: pm2 keep-alive service + global jevrun CLI around jevrun@0.0.2
---
.gitignore | 5 ++++
bin/jevrun.mjs | 34 ++++++++++++++++++++++++++
index.mjs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
package-lock.json | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
package.json | 21 ++++++++++++++++
5 files changed, 203 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8699ee1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+.env*
+*.log
+tmp/
+.DS_Store
diff --git a/bin/jevrun.mjs b/bin/jevrun.mjs
new file mode 100755
index 0000000..88cc754
--- /dev/null
+++ b/bin/jevrun.mjs
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+// Global `jevrun` CLI — talks to the always-alive jevrun-runner service.
+// jevrun health
+// jevrun run <url> "<natural-language task>"
+// jevrun "<task>" (no url; task runs on about:blank)
+const PORT = Number(process.env.JEVRUN_PORT || 9788);
+const HOST = process.env.JEVRUN_HOST || "127.0.0.1";
+const base = `http://${HOST}:${PORT}`;
+const [, , cmd, ...rest] = process.argv;
+
+async function main() {
+ if (cmd === "health" || cmd === "--health") {
+ const r = await fetch(`${base}/health`).catch(() => null);
+ if (!r) { console.error(`jevrun-runner not reachable at ${base} — is pm2 process 'jevrun-runner' up?`); process.exit(1); }
+ console.log(JSON.stringify(await r.json(), null, 2));
+ return;
+ }
+ let url, task;
+ if (cmd === "run") { url = rest[0]; task = rest.slice(1).join(" "); }
+ else { task = [cmd, ...rest].filter(Boolean).join(" "); }
+ if (!task) {
+ console.error('usage: jevrun run <url> "<task>" | jevrun "<task>" | jevrun health');
+ process.exit(2);
+ }
+ const r = await fetch(`${base}/run`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ url, task }),
+ }).catch((e) => { console.error(`request failed: ${e.message}`); process.exit(1); });
+ const body = await r.json();
+ console.log(JSON.stringify(body, null, 2));
+ process.exit(r.ok ? 0 : 1);
+}
+main();
diff --git a/index.mjs b/index.mjs
new file mode 100644
index 0000000..0dc93c7
--- /dev/null
+++ b/index.mjs
@@ -0,0 +1,71 @@
+// 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); });
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..5671e3e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,72 @@
+{
+ "name": "jevrun-runner",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "jevrun-runner",
+ "version": "0.1.0",
+ "dependencies": {
+ "jevrun": "0.0.2",
+ "playwright": "^1.63.0"
+ },
+ "bin": {
+ "jevrun": "bin/jevrun.mjs"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@typesafe-ai/sdk": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@typesafe-ai/sdk/-/sdk-0.6.0.tgz",
+ "integrity": "sha512-IddX+Q0XM+VagOUZFeP7wZjaO4SHMdvnh2zEBdrZZnXedWI3BNK1lKhMx3ayrkFWvVLbVcUHJy6AVZlY+e6Jaw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/jevrun": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/jevrun/-/jevrun-0.0.2.tgz",
+ "integrity": "sha512-XqBDvswpXxb4C7LAgWrOkQYeVibEpcbGrSPW7Nk6T6xfSYiajrzPgt53ptIIehKKp2rMpQxwHSFVFiR7hAPP9A==",
+ "dependencies": {
+ "@typesafe-ai/sdk": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "peerDependencies": {
+ "playwright-core": "^1.59.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
+ "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.63.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
+ "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e68d48d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "jevrun-runner",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "description": "Persistent keep-alive HTTP service + global `jevrun` CLI wrapping jevrun's run(page, prompt) on a live Playwright browser.",
+ "bin": {
+ "jevrun": "./bin/jevrun.mjs"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "scripts": {
+ "start": "node index.mjs",
+ "health": "curl -fsS http://127.0.0.1:9788/health"
+ },
+ "dependencies": {
+ "jevrun": "0.0.2",
+ "playwright": "^1.63.0"
+ }
+}
(oldest)
·
back to Jevrun Runner
·
(newest)