@melaya/runner 1.1.14 → 1.1.17

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.
@@ -272,17 +272,25 @@ def _apply_browser_turn_grant(req) -> bool:
272
272
  present (injected by the runner's per-turn bridge registration). These are
273
273
  purged at turn end alongside the grant (see _purge_browser_turn_grant)."""
274
274
  grant = str((req or {}).get("browser_grant") or "") if isinstance(req, dict) else ""
275
+ # Front 3: EXTENSION-target proxy turn — no client-side grant; the host's
276
+ # browser tools call the server bridge (MEL_BROWSER_URL/TOKEN) which holds the
277
+ # grant and runs the action against the user's real tab.
278
+ ext_proxy = bool((req or {}).get("browser_ext_proxy")) if isinstance(req, dict) else False
275
279
  target_ref = str((req or {}).get("browser_target_ref") or "") if isinstance(req, dict) else ""
276
- if grant and _browser_capable():
277
- os.environ["MEL_BROWSER_TURN_GRANT"] = grant
280
+ bridge_url = str((req or {}).get("mel_browser_url") or "") if isinstance(req, dict) else ""
281
+ bridge_token = str((req or {}).get("mel_browser_token") or "") if isinstance(req, dict) else ""
282
+ if (grant or ext_proxy) and _browser_capable():
283
+ if grant:
284
+ os.environ["MEL_BROWSER_TURN_GRANT"] = grant
285
+ else:
286
+ os.environ.pop("MEL_BROWSER_TURN_GRANT", None) # proxy: grant stays server-side
278
287
  if target_ref:
279
288
  os.environ["MEL_BROWSER_TURN_TARGET_REF"] = target_ref
280
289
  else:
281
290
  os.environ.pop("MEL_BROWSER_TURN_TARGET_REF", None)
282
- # Per-turn bridge URL + token (assistant path only; pipeline path has
283
- # these in the spawn env already). Apply only when present on the frame.
284
- bridge_url = str((req or {}).get("mel_browser_url") or "") if isinstance(req, dict) else ""
285
- bridge_token = str((req or {}).get("mel_browser_token") or "") if isinstance(req, dict) else ""
291
+ # Per-turn bridge URL + token (assistant path). For the runner target this
292
+ # is the runner's local bridge; for the extension proxy it is the server's
293
+ # /ext-bridge. Apply only when present on the frame.
286
294
  if bridge_url:
287
295
  os.environ["MEL_BROWSER_URL"] = bridge_url
288
296
  if bridge_token:
@@ -59,51 +59,56 @@ const MELAYA_BRAND_SCRIPT = `(() => {
59
59
  if (document.getElementById(ID + "_border")) return;
60
60
  const style = document.createElement("style");
61
61
  style.id = ID + "_style";
62
- 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-family:-apple-system,Segoe UI,Roboto,sans-serif;font-weight:600;font-size:12px;line-height:16px;white-space:nowrap;max-width:calc(100vw - 24px);box-sizing:border-box;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))}@media (max-width:600px){#" + ID + "_badge{font-size:10px;line-height:13px;padding:4px 10px 4px 6px;gap:6px;top:6px}#" + ID + "_logo{height:13px}}";
62
+ 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-family:-apple-system,Segoe UI,Roboto,sans-serif;font-weight:600;font-size:12px;line-height:1;white-space:nowrap;max-width:calc(100vw - 24px);box-sizing:border-box;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))}#" + ID + "_label{display:inline-flex;align-items:center;height:16px;line-height:1}@media (max-width:600px){#" + ID + "_badge{font-size:10px;padding:4px 10px 4px 6px;gap:6px;top:6px}#" + ID + "_logo{height:13px}#" + ID + "_label{height:13px}}";
63
63
  (document.head || root).appendChild(style);
64
64
  const border = document.createElement("div"); border.id = ID + "_border";
65
65
  const badge = document.createElement("div"); badge.id = ID + "_badge";
66
66
  const logo = document.createElement("img"); logo.id = ID + "_logo"; logo.src = LOGO; logo.alt = "Melaya";
67
- badge.appendChild(logo); badge.appendChild(document.createTextNode("Controlled by Melaya"));
67
+ const label = document.createElement("span"); label.id = ID + "_label"; label.textContent = "Controlled by Melaya";
68
+ badge.appendChild(logo); badge.appendChild(label);
68
69
  root.appendChild(border); root.appendChild(badge);
69
70
  };
70
71
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", inject); else inject();
71
72
  try { new MutationObserver(() => { if (!document.getElementById(ID + "_border")) inject(); }).observe(document.documentElement, { childList: true }); } catch (e) {}
72
73
  })();`;
73
74
  // ── Stealth init script ────────────────────────────────────────────────────
74
- // Runs in EVERY frame BEFORE page scripts (context.addInitScript). A headed
75
- // Brave/Chrome already carries a genuine fingerprint, so this only patches the
76
- // handful of vectors a CDP-driven session can still leak (webdriver flag, an
77
- // inconsistent Notification permission, a missing chrome stub, empty plugins,
78
- // masked WebGL vendor). It cannot hide the CDP transport itself — that is why
79
- // behaviour is also humanised below but it defeats the common JS bot checks.
75
+ // Runs in EVERY frame BEFORE page scripts (context.addInitScript).
76
+ //
77
+ // CRITICAL: this is a HEADED real Brave/Chrome, so its fingerprint (plugins,
78
+ // GPU, cores, memory, UA) is ALREADY genuine. Overriding those would INTRODUCE
79
+ // inconsistencies a detector catches (e.g. a spoofed WebGL vendor that disagrees
80
+ // with the real driver, or a patched function whose toString is not native).
81
+ // So we touch ONLY the two things a CDP session genuinely leaks:
82
+ // 1. navigator.webdriver (can read truthy under CDP), and
83
+ // 2. the Notification.permission vs permissions.query mismatch CDP introduces.
84
+ // Any function we replace is given a native-looking toString so the patch is not
85
+ // itself a tell. We do NOT fake plugins/webgl/hardware/languages/chrome — a real
86
+ // headed browser already has correct, self-consistent values.
80
87
  const STEALTH_SCRIPT = `(() => {
81
88
  const def = (o, k, get) => { try { Object.defineProperty(o, k, { get, configurable: true }); } catch (e) {} };
82
- // navigator.webdriver false (belt-and-suspenders; the launch flag already
83
- // disables it, but some builds still expose it truthy under CDP).
89
+ const nativeToString = (fn, name) => {
90
+ try {
91
+ const s = "function " + name + "() { [native code] }";
92
+ Object.defineProperty(fn, "toString", { value: function () { return s; }, configurable: true, writable: true });
93
+ } catch (e) {}
94
+ };
95
+ // 1. navigator.webdriver → false. The launch flag disables it, but some CDP
96
+ // builds still expose it truthy; keep it false as a belt-and-suspenders.
84
97
  def(Object.getPrototypeOf(navigator) || navigator, "webdriver", () => false);
85
98
  try { delete Navigator.prototype.webdriver; } catch (e) {}
86
- // Minimal chrome runtime stub when absent (headless-style gap).
87
- try { window.chrome = window.chrome || {}; window.chrome.runtime = window.chrome.runtime || {}; } catch (e) {}
88
- def(navigator, "languages", () => ["en-US", "en"]);
89
- // Non-empty plugins/mimeTypes so length checks pass.
90
- try { if (!navigator.plugins || navigator.plugins.length === 0) def(navigator, "plugins", () => [1, 2, 3, 4, 5]); } catch (e) {}
91
- // Notification permission must agree between Notification.permission and
92
- // permissions.query — a classic mismatch CDP introduces.
99
+ // 2. Notification.permission must agree with permissions.query (CDP mismatch).
93
100
  try {
94
101
  const q = navigator.permissions && navigator.permissions.query ? navigator.permissions.query.bind(navigator.permissions) : null;
95
- if (q) navigator.permissions.query = (p) => (p && p.name === "notifications")
96
- ? Promise.resolve({ state: (typeof Notification !== "undefined" ? Notification.permission : "default"), onchange: null })
97
- : q(p);
98
- } catch (e) {}
99
- // Mask WebGL UNMASKED_VENDOR/RENDERER (37445/37446) to a plausible GPU.
100
- try {
101
- const patch = (proto) => { if (!proto) return; const gp = proto.getParameter; if (!gp) return; proto.getParameter = function (p) { if (p === 37445) return "Intel Inc."; if (p === 37446) return "Intel Iris OpenGL Engine"; return gp.call(this, p); }; };
102
- patch(window.WebGLRenderingContext && WebGLRenderingContext.prototype);
103
- patch(window.WebGL2RenderingContext && WebGL2RenderingContext.prototype);
102
+ if (q) {
103
+ const patched = function (p) {
104
+ return (p && p.name === "notifications")
105
+ ? Promise.resolve({ state: (typeof Notification !== "undefined" ? Notification.permission : "default"), onchange: null })
106
+ : q(p);
107
+ };
108
+ nativeToString(patched, "query");
109
+ navigator.permissions.query = patched;
110
+ }
104
111
  } catch (e) {}
105
- def(navigator, "hardwareConcurrency", () => 8);
106
- def(navigator, "deviceMemory", () => 8);
107
112
  })();`;
108
113
  // ── Humanized input helpers ────────────────────────────────────────────────
109
114
  // Instant teleport-clicks and zero-delay typing are a behavioural bot tell.
@@ -458,13 +463,17 @@ export async function startBrowserBridge(opts) {
458
463
  // Normalise engine to a known BrowserEngineId; default to "chrome".
459
464
  const knownEngines = new Set(["chrome", "edge", "brave", "chromium"]);
460
465
  const engineId = (knownEngines.has(engine) ? engine : "chrome");
461
- // Resolve profile directory, mirroring simpleBrowserLauncher semantics:
462
- // "dedicated" => ephemeral per-session dir under tmpdir
463
- // anything else => persistent named dir under ~/.melaya-runner/browser-profiles/
464
- // spaceUserDataDir() already handles ephemeral vs persistent; map to SpaceSpec.
465
- const spaceSpec = profile === "dedicated"
466
- ? { kind: "ephemeral" }
467
- : { kind: "persistent", id: `browser-profiles-${engineId}-${profile}` };
466
+ // Resolve profile directory. The dedicated Melaya profile is PERSISTENT (a
467
+ // stable per-engine dir under ~/.melaya-runner/browser-profiles/), NOT a
468
+ // fresh per-session temp dir. Two reasons, both about real-world usability
469
+ // and anti-bot survival:
470
+ // 1. Logins persist. A fresh profile forces the user to sign in to every
471
+ // site on every launch, under bot scrutiny — the "can't log in" pain.
472
+ // 2. A cookieless, history-less profile is itself a strong bot signal.
473
+ // A warmed profile that accrues real cookies/history looks human.
474
+ // It is still isolated from the user's PERSONAL browser profile (separate
475
+ // dir), so this is a dedicated-but-warm profile, not their everyday one.
476
+ const spaceSpec = { kind: "persistent", id: `browser-profiles-${engineId}-${profile}` };
468
477
  // Reject duplicate launches for the same sessionId.
469
478
  if (interactiveSessions.has(sessionId)) {
470
479
  log(`[browser-launch] sessionId=${sessionId.slice(0, 16)} already active; ignoring duplicate launch`);
@@ -1569,6 +1569,16 @@ export async function connect(opts) {
1569
1569
  return;
1570
1570
  }
1571
1571
  }
1572
+ else if (payload.browserExtProxy && payload.browserExtProxy.url && payload.browserExtProxy.token) {
1573
+ // Front 3: EXTENSION target. No grant verification / local bridge run — the
1574
+ // host's browser_* tools POST to the server bridge (Bearer proxy token) and
1575
+ // the SERVER runs the action against the extension using the grant it holds.
1576
+ turnBridgeUrl = String(payload.browserExtProxy.url);
1577
+ turnBridgeToken = String(payload.browserExtProxy.token);
1578
+ browserTargetRef = String(payload.browserExtProxy.targetRef || "");
1579
+ if (opts.verbose)
1580
+ console.log(chalk.gray(` [browser-bridge] ext-proxy turn (session=${payload.sessionId.slice(0, 10)} url=${turnBridgeUrl})`));
1581
+ }
1572
1582
  // Capture for the stdout "done" handler closure below.
1573
1583
  const _turnBridgeRunId = turnBridgeRunId;
1574
1584
  const _turnId = payload.turnId;
@@ -1617,9 +1627,13 @@ export async function connect(opts) {
1617
1627
  // JWS string so the Python browser toolkit can verify it again
1618
1628
  // (defence-in-depth) and install MEL_BROWSER_TURN_GRANT in the
1619
1629
  // host env for THIS turn only. Absent when no grant was provided.
1620
- ...(verifiedGrantToken ? { browser_grant: verifiedGrantToken, browser_target_ref: browserTargetRef ?? "" } : {}),
1630
+ ...(verifiedGrantToken
1631
+ ? { browser_grant: verifiedGrantToken, browser_target_ref: browserTargetRef ?? "" }
1632
+ // Front 3 EXTENSION proxy: no grant, but the host must still apply
1633
+ // MEL_BROWSER_URL/TOKEN + register the browser toolkit for the turn.
1634
+ : (turnBridgeUrl ? { browser_ext_proxy: true, browser_target_ref: browserTargetRef ?? "" } : {})),
1621
1635
  // Bridge URL + token for the host process env (applied per-turn by
1622
- // _apply_browser_turn_grant alongside the grant token).
1636
+ // _apply_browser_turn_grant, alongside the grant token OR the proxy flag).
1623
1637
  ...(turnBridgeUrl && turnBridgeToken
1624
1638
  ? { mel_browser_url: turnBridgeUrl, mel_browser_token: turnBridgeToken }
1625
1639
  : {}),
package/package.json CHANGED
@@ -1,44 +1,44 @@
1
- {
2
- "name": "@melaya/runner",
3
- "version": "1.1.14",
4
- "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
- "license": "UNLICENSED",
6
- "private": false,
7
- "type": "module",
8
- "bin": {
9
- "melaya-runner": "dist/cli.js"
10
- },
11
- "main": "dist/index.js",
12
- "files": [
13
- "dist/**/*.js",
14
- "dist/**/*.d.ts",
15
- "dist/**/*.py",
16
- "localRagIngest.py",
17
- "localRagRetrieve.py",
18
- "nltk_data/**",
19
- "README.md"
20
- ],
21
- "scripts": {
22
- "build": "tsc && node -e \"const fs=require('fs'); fs.copyFileSync('localRagIngest.py','dist/localRagIngest.py'); fs.copyFileSync('localRagRetrieve.py','dist/localRagRetrieve.py'); fs.copyFileSync('src/assistantHost.py','dist/assistantHost.py')\"",
23
- "test": "node --test --import tsx \"src/**/*.test.ts\"",
24
- "prepublishOnly": "npm run build"
25
- },
26
- "dependencies": {
27
- "chalk": "^5.3.0",
28
- "commander": "^12.0.0",
29
- "ora": "^8.0.0",
30
- "playwright": "^1.48.0",
31
- "socket.io-client": "^4.8.0"
32
- },
33
- "devDependencies": {
34
- "@types/node": "^20.0.0",
35
- "tsx": "^4.19.2",
36
- "typescript": "^5.5.0"
37
- },
38
- "engines": {
39
- "node": ">=18"
40
- },
41
- "publishConfig": {
42
- "access": "public"
43
- }
44
- }
1
+ {
2
+ "name": "@melaya/runner",
3
+ "version": "1.1.17",
4
+ "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "type": "module",
8
+ "bin": {
9
+ "melaya-runner": "dist/cli.js"
10
+ },
11
+ "main": "dist/index.js",
12
+ "files": [
13
+ "dist/**/*.js",
14
+ "dist/**/*.d.ts",
15
+ "dist/**/*.py",
16
+ "localRagIngest.py",
17
+ "localRagRetrieve.py",
18
+ "nltk_data/**",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc && node -e \"const fs=require('fs'); fs.copyFileSync('localRagIngest.py','dist/localRagIngest.py'); fs.copyFileSync('localRagRetrieve.py','dist/localRagRetrieve.py'); fs.copyFileSync('src/assistantHost.py','dist/assistantHost.py')\"",
23
+ "test": "node --test --import tsx \"src/**/*.test.ts\"",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "dependencies": {
27
+ "chalk": "^5.3.0",
28
+ "commander": "^12.0.0",
29
+ "ora": "^8.0.0",
30
+ "playwright": "^1.48.0",
31
+ "socket.io-client": "^4.8.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.0.0",
35
+ "tsx": "^4.19.2",
36
+ "typescript": "^5.5.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }