@melaya/runner 1.1.13 → 1.1.16

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.
Files changed (2) hide show
  1. package/dist/browserBridge.js +158 -19
  2. package/package.json +44 -44
@@ -59,17 +59,136 @@ 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: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))}@media (max-width:600px){#" + ID + "_badge{font-size:10px;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
  })();`;
74
+ // ── Stealth init script ────────────────────────────────────────────────────
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.
87
+ const STEALTH_SCRIPT = `(() => {
88
+ const def = (o, k, get) => { try { Object.defineProperty(o, k, { get, configurable: true }); } catch (e) {} };
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.
97
+ def(Object.getPrototypeOf(navigator) || navigator, "webdriver", () => false);
98
+ try { delete Navigator.prototype.webdriver; } catch (e) {}
99
+ // 2. Notification.permission must agree with permissions.query (CDP mismatch).
100
+ try {
101
+ const q = navigator.permissions && navigator.permissions.query ? navigator.permissions.query.bind(navigator.permissions) : null;
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
+ }
111
+ } catch (e) {}
112
+ })();`;
113
+ // ── Humanized input helpers ────────────────────────────────────────────────
114
+ // Instant teleport-clicks and zero-delay typing are a behavioural bot tell.
115
+ // These move the pointer along an eased, jittered Bezier path, dwell before
116
+ // pressing, type with variable per-key cadence, and scroll in small steps.
117
+ // (PWPage is already aliased above to import("playwright").Page.)
118
+ const _rand = (min, max) => min + Math.random() * (max - min);
119
+ const _randInt = (min, max) => Math.floor(_rand(min, max + 1));
120
+ const _lastMouse = new WeakMap();
121
+ async function humanPause(page, min = 40, max = 140) {
122
+ await page.waitForTimeout(_randInt(min, max));
123
+ }
124
+ // Eased, jittered pointer travel from the last known position to (tx,ty).
125
+ async function humanMove(page, tx, ty) {
126
+ const vp = page.viewportSize() ?? { width: 1280, height: 800 };
127
+ const from = _lastMouse.get(page) ?? { x: _rand(0, vp.width), y: _rand(0, vp.height) };
128
+ const dist = Math.hypot(tx - from.x, ty - from.y);
129
+ const steps = Math.max(6, Math.min(42, Math.round(dist / _rand(18, 34))));
130
+ const curve = Math.min(90, dist * 0.28);
131
+ const midX = (from.x + tx) / 2 + _rand(-1, 1) * curve;
132
+ const midY = (from.y + ty) / 2 + _rand(-1, 1) * curve;
133
+ for (let i = 1; i <= steps; i++) {
134
+ const t = i / steps;
135
+ const e = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; // easeInOutQuad
136
+ const mt = 1 - e;
137
+ let x = mt * mt * from.x + 2 * mt * e * midX + e * e * tx;
138
+ let y = mt * mt * from.y + 2 * mt * e * midY + e * e * ty;
139
+ if (i < steps) {
140
+ x += _rand(-1.2, 1.2);
141
+ y += _rand(-1.2, 1.2);
142
+ } // hand tremor
143
+ await page.mouse.move(x, y);
144
+ if (i % _randInt(3, 5) === 0)
145
+ await page.waitForTimeout(_randInt(4, 16));
146
+ }
147
+ _lastMouse.set(page, { x: tx, y: ty });
148
+ }
149
+ // Move to a point a few px off the exact center, dwell, then press/release.
150
+ async function humanClick(page, x, y, clickCount = 1) {
151
+ const jx = x + _rand(-3, 3), jy = y + _rand(-3, 3);
152
+ await humanMove(page, jx, jy);
153
+ await humanPause(page, 30, 120);
154
+ for (let c = 0; c < clickCount; c++) {
155
+ await page.mouse.down();
156
+ await page.waitForTimeout(_randInt(40, 110)); // press dwell
157
+ await page.mouse.up();
158
+ if (c + 1 < clickCount)
159
+ await page.waitForTimeout(_randInt(60, 130));
160
+ }
161
+ _lastMouse.set(page, { x: jx, y: jy });
162
+ }
163
+ // Type with variable cadence: slower on spaces, longer at punctuation, and an
164
+ // occasional "thinking" pause.
165
+ async function humanType(page, text) {
166
+ for (const ch of text) {
167
+ await page.keyboard.type(ch);
168
+ let d = _rand(45, 150);
169
+ if (ch === " ")
170
+ d += _rand(10, 60);
171
+ if (/[.,!?;:]/.test(ch))
172
+ d += _rand(80, 220);
173
+ if (Math.random() < 0.04)
174
+ d += _rand(180, 450);
175
+ await page.waitForTimeout(Math.round(d));
176
+ }
177
+ }
178
+ // Break a wheel scroll into a few smaller, jittered steps with pauses.
179
+ async function humanScroll(page, dx, dy) {
180
+ const steps = _randInt(3, 6);
181
+ let sx = 0, sy = 0;
182
+ for (let i = 0; i < steps; i++) {
183
+ const px = Math.round(dx / steps + _rand(-8, 8));
184
+ const py = Math.round(dy / steps + _rand(-14, 14));
185
+ await page.mouse.wheel(px, py);
186
+ sx += px;
187
+ sy += py;
188
+ await page.waitForTimeout(_randInt(40, 130));
189
+ }
190
+ return { dx: sx, dy: sy };
191
+ }
73
192
  const MAX_BODY_BYTES = 512 * 1024; // /browser/run scripts included
74
193
  const NODE_CAP = 240; // snapshot node cap (Section 8)
75
194
  const SCREENSHOT_MAX_B64 = 1_400_000; // ~1 MB binary; ladder recompresses
@@ -260,17 +379,24 @@ export async function startBrowserBridge(opts) {
260
379
  headless: reg.spec.headless === true,
261
380
  viewport: { width: 1280, height: 800 },
262
381
  acceptDownloads: false, // downloads default-denied (Section 10)
382
+ ignoreDefaultArgs: ["--enable-automation"],
263
383
  args: [
264
384
  "--no-first-run",
265
385
  "--no-default-browser-check",
266
386
  "--disable-background-networking",
267
387
  "--disable-sync",
388
+ "--disable-blink-features=AutomationControlled",
268
389
  ],
269
390
  });
270
391
  sessions.attachHandles(rec, {
271
392
  context,
272
393
  ephemeralUserDataDir: ephemeral ? userDataDir : null,
273
394
  });
395
+ // Stealth patches before any page script (see STEALTH_SCRIPT).
396
+ try {
397
+ await context.addInitScript(STEALTH_SCRIPT);
398
+ }
399
+ catch { /* non-fatal */ }
274
400
  await enforceOnContext(context, reg.policy, hooks);
275
401
  const page = context.pages()[0] ?? (await context.newPage());
276
402
  sessions.leaseTarget(rec, reg.spec.grant.target.ref, page);
@@ -337,13 +463,17 @@ export async function startBrowserBridge(opts) {
337
463
  // Normalise engine to a known BrowserEngineId; default to "chrome".
338
464
  const knownEngines = new Set(["chrome", "edge", "brave", "chromium"]);
339
465
  const engineId = (knownEngines.has(engine) ? engine : "chrome");
340
- // Resolve profile directory, mirroring simpleBrowserLauncher semantics:
341
- // "dedicated" => ephemeral per-session dir under tmpdir
342
- // anything else => persistent named dir under ~/.melaya-runner/browser-profiles/
343
- // spaceUserDataDir() already handles ephemeral vs persistent; map to SpaceSpec.
344
- const spaceSpec = profile === "dedicated"
345
- ? { kind: "ephemeral" }
346
- : { 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}` };
347
477
  // Reject duplicate launches for the same sessionId.
348
478
  if (interactiveSessions.has(sessionId)) {
349
479
  log(`[browser-launch] sessionId=${sessionId.slice(0, 16)} already active; ignoring duplicate launch`);
@@ -401,6 +531,14 @@ export async function startBrowserBridge(opts) {
401
531
  // all navigations and new pages. The one-shot evaluate covers the
402
532
  // already-open page (typically about:blank) immediately.
403
533
  // Both are wrapped so a branding failure cannot fail the launch.
534
+ // Stealth FIRST (runs before page + brand scripts in every frame) so the
535
+ // navigator/webgl patches are in place before any site fingerprinting.
536
+ try {
537
+ await context.addInitScript(STEALTH_SCRIPT);
538
+ }
539
+ catch (stealthErr) {
540
+ log(`[browser-launch] stealth init-script install failed (non-fatal): ${stealthErr?.message || stealthErr}`);
541
+ }
404
542
  try {
405
543
  await context.addInitScript(MELAYA_BRAND_SCRIPT);
406
544
  }
@@ -852,16 +990,16 @@ export async function startBrowserBridge(opts) {
852
990
  }
853
991
  const pt = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
854
992
  if (kind === "hover")
855
- await page.mouse.move(pt.x, pt.y);
993
+ await humanMove(page, pt.x, pt.y);
856
994
  else
857
- await page.mouse.click(pt.x, pt.y, { clickCount: kind === "dblclick" ? 2 : 1 });
995
+ await humanClick(page, pt.x, pt.y, kind === "dblclick" ? 2 : 1);
858
996
  return { at: { x: Math.round(pt.x), y: Math.round(pt.y) }, matched_text: label };
859
997
  }
860
998
  const pt = await resolveActionPoint(reg, rec, lease, args);
861
999
  if (kind === "hover")
862
- await page.mouse.move(pt.x, pt.y);
1000
+ await humanMove(page, pt.x, pt.y);
863
1001
  else
864
- await page.mouse.click(pt.x, pt.y, { clickCount: kind === "dblclick" ? 2 : 1 });
1002
+ await humanClick(page, pt.x, pt.y, kind === "dblclick" ? 2 : 1);
865
1003
  return { at: { x: Math.round(pt.x), y: Math.round(pt.y) } };
866
1004
  }
867
1005
  case "tap": {
@@ -869,7 +1007,7 @@ export async function startBrowserBridge(opts) {
869
1007
  // browser.py sends {x, y} as 0..1 fractions or CSS px.
870
1008
  // resolveActionPoint already handles the fraction/px heuristic.
871
1009
  const pt = await resolveActionPoint(reg, rec, lease, args);
872
- await page.mouse.click(pt.x, pt.y);
1010
+ await humanClick(page, pt.x, pt.y);
873
1011
  return { at: { x: Math.round(pt.x), y: Math.round(pt.y) } };
874
1012
  }
875
1013
  case "input_text": {
@@ -877,10 +1015,11 @@ export async function startBrowserBridge(opts) {
877
1015
  if (text.length > 20_000)
878
1016
  throw new BridgeError("act_args_invalid", "text exceeds 20000 chars");
879
1017
  const pt = await resolveActionPoint(reg, rec, lease, args);
880
- await page.mouse.click(pt.x, pt.y);
881
- // Clear-then-type: select-all + type preserves site key handlers.
1018
+ await humanClick(page, pt.x, pt.y);
1019
+ // Clear-then-type: select-all + humanized type preserves site key handlers.
882
1020
  await page.keyboard.press(process.platform === "darwin" ? "Meta+A" : "Control+A").catch(() => { });
883
- await page.keyboard.type(text, { delay: 8 });
1021
+ await humanPause(page, 40, 120);
1022
+ await humanType(page, text);
884
1023
  return { typed: text.length };
885
1024
  }
886
1025
  case "press_key": {
@@ -920,8 +1059,8 @@ export async function startBrowserBridge(opts) {
920
1059
  else {
921
1060
  dy = Math.max(-4000, Math.min(4000, Number(args.dy ?? 600)));
922
1061
  }
923
- await page.mouse.wheel(dx, dy);
924
- return { scrolled: { dx, dy } };
1062
+ const scrolled = await humanScroll(page, dx, dy);
1063
+ return { scrolled };
925
1064
  }
926
1065
  case "get_text": {
927
1066
  // Fix 1: read-only page-text extraction.
package/package.json CHANGED
@@ -1,44 +1,44 @@
1
- {
2
- "name": "@melaya/runner",
3
- "version": "1.1.13",
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.16",
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
+ }