@melaya/runner 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -78,6 +78,18 @@ export interface BrowserBridge {
78
78
  * (looked up by sessionId directly).
79
79
  */
80
80
  setWatchLease(sessionId: string, active: boolean, framePostUrl: string, runAuthHeader: string): void;
81
+ /**
82
+ * Close one or all interactive sessions (emergency stop / per-session close).
83
+ *
84
+ * When sessionId is provided, only that interactive session is closed.
85
+ * When sessionId is omitted (or undefined), ALL active interactive sessions
86
+ * on this runner are closed.
87
+ *
88
+ * Returns the array of sessionIds that were actually closed. Idempotent;
89
+ * per-session errors are swallowed (best-effort). Does NOT touch
90
+ * grant-scoped run sessions (byRunId).
91
+ */
92
+ killInteractive(sessionId?: string): Promise<string[]>;
81
93
  shutdown(): Promise<void>;
82
94
  }
83
95
  export declare function startBrowserBridge(opts: {
@@ -41,6 +41,32 @@ import { runSandboxedScript } from "./codeWorker.js";
41
41
  // ---------------------------------------------------------------------
42
42
  // Limits and constants
43
43
  // ---------------------------------------------------------------------
44
+ // Injected into every interactive-session page to display a non-interactive
45
+ // Melaya branding overlay (pill badge with logo + pulsing cyan border).
46
+ // Defined once here so it is shared between context.addInitScript (persistent
47
+ // across navigations) and the one-shot page.evaluate on the already-open page.
48
+ // Palette: cyan #22D3EE primary accent, indigo #7C6FF0 subtle secondary,
49
+ // slate/navy #0F172A base. Less purple than the prior version.
50
+ const MELAYA_BRAND_SCRIPT = `(() => {
51
+ if (window.__melayaBrand) return; window.__melayaBrand = true;
52
+ const ID = "__melaya_brand";
53
+ const LOGO = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAsCAMAAADGmMENAAADAFBMVEVMaXEATOaL9vsBTesAEokAF5oAEo0AY9sAHafj/f8ADogAUucAfuoARscAXusAeOce1/8Ac+UAb+QTzv8D2P8CoPfT9v8BCHIAEpMAF5YASsoAQMUAT80AHq4B1f8A0/8ENska0v8FGn8BU9Dy//8AFpECauEAg+0Bie4AO8EBkfIAlfUAGqIAzv8i3f8AXNbM8f8HLosBO+RB7/8CjfBd7P598P4Lre8GQ9cH3P8Ev/067f8w6f8AL7MAObcACnsBU9gHpe4Aaugo4f8FaPBK8f8JxP0AJ7cA0f8ArfoP2//a+/8ADYIDStIBjOgQvPQBi/QInugBhOkKf9sAWNIBgOXe+/8Ltf/r//8AZduf8/4ktfctt/p08P1I1P0Rw/gEleoVyvoAZN8AYtwGadQFm+8Sv/4Cb/ABWdwCQ80AJq8EYeiL8P9r8v912/whsegkeLcMtfEAXuACY+QAWOUBfu0ClO800f3E8v8AZ90+4/+64v4AReyWu/gBSNT5//4u4P8WPoRf0/MNuPkHoOwnxfsas/EAa+TO+/YFie0CQ8sWxvtJjvgAT9QCJ4QAZuxhv+8DkN0AHIoomOoUnfkQS5MdfsgML30CeNMuXJlQib9P5P6m9/sAOtQANs0ALL+h+PwAYuMIne4ZwfUBj+8Oq+yn1/2e+P0BcOp+vfvP+Pw8ofgTaO1no/YHTMOazfsEW6Vg4PwAK7Uck/qI9v5Gca09wPxim8s4ZKkIPpkCfuMBbuNb6v0AYuRe7PyL9v0AhfOL8vJO5fsBmvQAV+aM9vwESNwALb1F4Pwp0fcAKLcAIaoEQtoXX/EAU88BW+cAGJkAfPAsdPMgfe8oaaLQ+vtTg7o/pdIzuul5sNh3vNwAY98PoOG27/4T3/8AWOkAGaABY+4Csv0BRuQCuf0k5v8EqvsBmvYCy/8PyP8DpPwHg/oDePQKsPYAML4Bdesb4/8AmvwBYdoIb7dP/P8BWfMAUPIAIJsAkfsTpOMARLUDY/Vostxr/v8r7v8zndiULzD1AAAA3nRSTlMA/gP+/v7+Av7+/v7+/v79/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/vz+/v3+/v7+/v7+/v7+/v79/v7+/v7+/f3+/v3+/gn+/P7+/v79/f76+v7+/v0gFP7+/v7++/79/f3+/v7+/vxC/vX+/v3+/v4z/v7+/v4t90pmEpFn/v7+/qr++P7++/7+/v7+/v5HUM6PaeZkptHN/o6A/ij+9/6j/v7+qv7q/v7+/v6xz7CH0LPFKoyow9Chu2PJfevd/t3c5L3+/v6a/v7+/qny6f5DYJsMAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFTklEQVR42oXVd1jSeRzA8Z8JCCEgIJi4Q4npDs+BkaKgYplmpqkt98ortdKu1Pbe++queXXta9/ee49u/KFAoiiouRtW9/n+0MrOuu/z8Dw8PK/f5/f+wcP3i2Ej15gx8FpzbOqJNSkYZmeHvWwBxdZ9/c3c1pD29hNHc17qkf3k08W+7Kmvhkyeltuyff0q5Ee7AE94/f1qo9fc2bieM2VKUuq7lWNG8ciue/PDoEcODs/oVIal8721zwehhLffmtj7KDra9xmdnMpgCqJURz6Y/zTImvBRaW94RATCbNDtIZNzcc0SLFrY2FextdLO6vGELx72BoUP642zVyJdNSXZHumKCqpNP/XztegbRQlXHvbGxCAdbdV5K9vbOblVwgn2nayoYirVBlZ/30+nIOiPDGRjgoLCI2KqG+JBt4Ju5rSIk0BH0YvP2sKyoVL7+86uxzLkJaVgYbSxZGbGQS9T68a8NENzW4skyWzJ/+1czWvLbPFFV6nMWNadg1klpUEREeFGube395Iyr7CmtLTmtq67ZvOZC/4JFKvuUy1SFrIwudE33uFSSVaQ8Y7cm0LxXjEjIxvHZ07XJCRw/RNA9z9QFRSKChM7QUc7sE2x2WXyCPmKmRQKZSa6Qea2k9wZXH9YlNf6HxQnijoAb2ZgWaC92LFNhsyy0gwK7uECCsVqExJqNlW4KNQiFxqPr7QH7evAbo0Na4rLnBhaK+eu8EZ+BpfL9YfpFzbZ9onUHSKRC891ul8yPOVTfachtGHiEm98MjeBsuT0r8sCb+tpCNNc+X7dQqS9hnUDcEfH6gxUQqk590Ng4G0bGxWtwwV1TO9OD8ay8B8E6VlIx4c6emhqS7gnN9nilkpV8VA0dKQvFWJXHsWz2a1hTbhuQLPd3WUeuwLBItzYmO8qAsz380vv3IX9M1hfawoLazLgGka7e2hkjsFgcUwHDU8Impnacx4re3zger1DmOGp1mhk7uKhyXQ63cJ34fH4rH0XAwaUWLZp473yA9sWp3Fw7QjZGpmHGE8GrNeD3izYd3HLgFqdiGU3GQzt28tvDV7LPBgKGoWYPCS3wSKs0lu6BXt2bxlwI0vJSMdxxOLc7T1X/5YbHd0hRCYzaSQ2jYD1sCy7wBLJunFEstsrWLYhjlOVzGKl7rt5Y/CS0UOG6xYqFOvpevPlPXVORKnP2HFEKdKLDc0twcnMdD7/5p/lN67XGzUmE+hGPbI7y+t2RI6fpB1LGtbNHInQnpU+nRbwl9eCnv236ms1sTIOna4Cu3eHoqNwvNNYpHVIr27jiJMZgm7QCwyGvJCe3Ve3VbtLHlze+TtYBVnnOVK3BNszl/rxkY7jcNJCeur2D157vH+8T1GRs7OUPKzxktVdEiEDQlxdQMOfN1jcNrlnb50TgTxAIDjryOQRs2dJxBOYEILrNo5ILLS3l1TdJ/potSRn4vM6L0mIh/BEAffQvhAM2wizk+akJRBGarzkWNtdhgBCaKBhW0CYwey8P4lAIPg80QTSOGed2vNLDFv1WZLFz5VHiwy41wUdCLOikCaBlj7Rburzy/G9bdV6RjFPFBkwrcs6mSWIihzS1tlagvS7N+bjFu2x808dUYKWDOF5i0ZqndVa92S01+as/fH+nKQh3L0w8kmJ1HPg529zRhwQ6H3KhuNdZisuKBjWRDfP7zfk/PcwQf7ocXP+vHkLC5QFikmAfXzIHYc3pIx+sMFndms+tkQplZuVCi2JRJKqDx9KecGhNuQrf8lvTExUaAm6oq8O2b3YDvVglVuLqQpi0TvL//cwtoLlW2mj238Bms49IJNu4nIAAAAASUVORK5CYII=";
54
+ const inject = () => {
55
+ const root = document.documentElement; if (!root) return;
56
+ if (document.getElementById(ID + "_border")) return;
57
+ const style = document.createElement("style");
58
+ style.id = ID + "_style";
59
+ style.textContent = "@keyframes melBrandPulse{0%,100%{box-shadow:inset 0 0 9px 2px rgba(34,211,238,.5),inset 0 0 30px 7px rgba(124,111,240,.18)}50%{box-shadow:inset 0 0 16px 4px rgba(34,211,238,.8),inset 0 0 54px 13px rgba(124,111,240,.32)}}@keyframes melBrandGlow{0%,100%{opacity:.78}50%{opacity:1}}#" + ID + "_border{position:fixed;inset:0;pointer-events:none;z-index:2147483647;border-radius:7px;animation:melBrandPulse 2.6s ease-in-out infinite}#" + ID + "_badge{position:fixed;top:10px;left:50%;transform:translateX(-50%);pointer-events:none;z-index:2147483647;font:600 12px/1 -apple-system,Segoe UI,Roboto,sans-serif;color:#E2E8F0;background:linear-gradient(180deg,rgba(15,23,42,.94),rgba(15,23,42,.82));padding:5px 13px 5px 7px;border-radius:999px;letter-spacing:.2px;border:1px solid rgba(34,211,238,.45);box-shadow:0 0 16px 1px rgba(34,211,238,.4),0 4px 14px rgba(0,0,0,.45);display:flex;align-items:center;gap:8px;backdrop-filter:blur(6px);animation:melBrandGlow 2.6s ease-in-out infinite}#" + ID + "_logo{height:16px;width:auto;display:block;filter:drop-shadow(0 0 4px rgba(34,211,238,.55))}";
60
+ (document.head || root).appendChild(style);
61
+ const border = document.createElement("div"); border.id = ID + "_border";
62
+ const badge = document.createElement("div"); badge.id = ID + "_badge";
63
+ const logo = document.createElement("img"); logo.id = ID + "_logo"; logo.src = LOGO; logo.alt = "Melaya";
64
+ badge.appendChild(logo); badge.appendChild(document.createTextNode("Controlled by Melaya"));
65
+ root.appendChild(border); root.appendChild(badge);
66
+ };
67
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", inject); else inject();
68
+ try { new MutationObserver(() => { if (!document.getElementById(ID + "_border")) inject(); }).observe(document.documentElement, { childList: true }); } catch (e) {}
69
+ })();`;
44
70
  const MAX_BODY_BYTES = 512 * 1024; // /browser/run scripts included
45
71
  const NODE_CAP = 240; // snapshot node cap (Section 8)
46
72
  const SCREENSHOT_MAX_B64 = 1_400_000; // ~1 MB binary; ladder recompresses
@@ -252,11 +278,15 @@ export async function startBrowserBridge(opts) {
252
278
  headless: false,
253
279
  viewport: { width: 1280, height: 800 },
254
280
  acceptDownloads: false,
281
+ // Suppress the "browser is being controlled by automated test
282
+ // software" infobar that Chromium/Brave shows by default.
283
+ ignoreDefaultArgs: ["--enable-automation"],
255
284
  args: [
256
285
  "--no-first-run",
257
286
  "--no-default-browser-check",
258
287
  "--disable-background-networking",
259
288
  "--disable-sync",
289
+ "--disable-blink-features=AutomationControlled",
260
290
  ],
261
291
  });
262
292
  }
@@ -272,6 +302,24 @@ export async function startBrowserBridge(opts) {
272
302
  context,
273
303
  ephemeralUserDataDir: ephemeral ? userDataDir : null,
274
304
  });
305
+ // Install Melaya branding overlay. addInitScript persists it across
306
+ // all navigations and new pages. The one-shot evaluate covers the
307
+ // already-open page (typically about:blank) immediately.
308
+ // Both are wrapped so a branding failure cannot fail the launch.
309
+ try {
310
+ await context.addInitScript(MELAYA_BRAND_SCRIPT);
311
+ }
312
+ catch (brandErr) {
313
+ log(`[browser-launch] branding init-script install failed (non-fatal): ${brandErr?.message || brandErr}`);
314
+ }
315
+ try {
316
+ const firstPage = context.pages()[0];
317
+ if (firstPage)
318
+ await firstPage.evaluate(MELAYA_BRAND_SCRIPT);
319
+ }
320
+ catch (brandErr) {
321
+ log(`[browser-launch] branding evaluate on first page failed (non-fatal): ${brandErr?.message || brandErr}`);
322
+ }
275
323
  log(`[browser-launch] interactive session active sessionId=${sessionId.slice(0, 16)} engine=${engineId}`);
276
324
  onState(sessionId, "active", engine);
277
325
  // Detect browser close: context 'close' fires when the user shuts
@@ -1161,6 +1209,38 @@ export async function startBrowserBridge(opts) {
1161
1209
  }),
1162
1210
  ]);
1163
1211
  };
1212
+ // killInteractive: close one or all interactive sessions (emergency stop).
1213
+ // Does NOT touch grant-scoped run sessions in byRunId.
1214
+ const killInteractive = async (sessionId) => {
1215
+ // Build the list of sessionIds to close.
1216
+ const targets = sessionId !== undefined
1217
+ ? (interactiveSessions.has(sessionId) ? [sessionId] : [])
1218
+ : [...interactiveSessions.keys()];
1219
+ const closed = [];
1220
+ await Promise.all(targets.map(async (id) => {
1221
+ try {
1222
+ // Stop any active watch-lease frame producer for this session.
1223
+ for (const [sid, wl] of watchLeases) {
1224
+ if (wl.runId === id)
1225
+ stopWatchLease(sid);
1226
+ }
1227
+ // Remove from the interactiveSessions map first so the context
1228
+ // "close" handler does not re-emit a redundant "closed" state.
1229
+ interactiveSessions.delete(id);
1230
+ // Tear down through sessionManager (closes the Playwright context,
1231
+ // cleans up leases, removes the session record).
1232
+ await sessions.teardownRun(id, "kill_interactive").catch(() => { });
1233
+ log(`[browser-kill] interactive session killed: ${id.slice(0, 16)}`);
1234
+ closed.push(id);
1235
+ }
1236
+ catch {
1237
+ // Best-effort per-session: a session that already closed or crashed
1238
+ // should not prevent other sessions from being killed.
1239
+ closed.push(id);
1240
+ }
1241
+ }));
1242
+ return closed;
1243
+ };
1164
1244
  return {
1165
1245
  url: `http://127.0.0.1:${port}`,
1166
1246
  port,
@@ -1173,6 +1253,7 @@ export async function startBrowserBridge(opts) {
1173
1253
  launchInteractive,
1174
1254
  getInteractiveSession,
1175
1255
  setWatchLease,
1256
+ killInteractive,
1176
1257
  async shutdown() {
1177
1258
  // Stop all watch-lease producers before closing sessions.
1178
1259
  for (const sid of [...watchLeases.keys()])
@@ -327,6 +327,39 @@ export async function connect(opts) {
327
327
  }
328
328
  });
329
329
  });
330
+ // ── BrowserControlPage emergency stop / per-session close (browser:kill) ──
331
+ // The server emits browser:kill { sessionId? } when the user presses the
332
+ // emergency-stop button (no sessionId = kill all) or closes a specific
333
+ // session card (sessionId present). The bridge closes the matching
334
+ // Playwright context(s) and returns the sessionIds it actually closed.
335
+ // For each closed session a browser:session { state: "closed" } is emitted
336
+ // so the server marks the DB row closed.
337
+ socket.on("browser:kill", async (payload) => {
338
+ const targetId = payload?.sessionId ? String(payload.sessionId) : undefined;
339
+ if (opts.verbose) {
340
+ console.log(chalk.gray(` [browser-kill] received sessionId=${targetId ? targetId.slice(0, 16) : "(all)"}`));
341
+ }
342
+ try {
343
+ const bridge = await _ensureBrowserBridge();
344
+ if (!bridge) {
345
+ console.log(chalk.yellow(" ! browser:kill: bridge unavailable"));
346
+ return;
347
+ }
348
+ const closedIds = await bridge.killInteractive(targetId);
349
+ for (const sid of closedIds) {
350
+ socket.emit("browser:session", { sessionId: sid, state: "closed", engine: "" });
351
+ if (opts.verbose) {
352
+ console.log(chalk.gray(` [browser-kill] browser:session sessionId=${sid.slice(0, 16)} state=closed`));
353
+ }
354
+ }
355
+ if (closedIds.length > 0) {
356
+ console.log(chalk.gray(` [browser-kill] closed ${closedIds.length} interactive session(s)`));
357
+ }
358
+ }
359
+ catch (e) {
360
+ console.log(chalk.yellow(` ! browser:kill error: ${e?.message || e}`));
361
+ }
362
+ });
330
363
  // ── Heartbeat ──────────────────────────────────────────────────────
331
364
  setInterval(() => {
332
365
  if (socket.connected)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,