[object Object]

← 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

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)