leapfrog-mcp 0.6.9 → 0.7.1

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.
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import { adaptiveNavigate, formatAdaptiveResult } from "./adaptive-wait.js";
27
27
  import { runStealthAudit } from "./stealth-audit.js";
28
28
  import { exportSession, replayRecording } from "./recording.js";
29
29
  import { paginate } from "./paginate.js";
30
- import { getHUDInitScript, getHUDUpdateScript, getClickRippleScript } from "./session-hud.js";
30
+ import { getHUDInitScript, getHUDUpdateScript, getClickRippleScript, getAgentEyesInitScript } from "./session-hud.js";
31
31
  import { getDetectionInitScript, getDetectionCheckScript, getResolutionCheckScript, parseDetectionResult, getPressAndHoldDetectScript, solvePressAndHold } from "./intervention.js";
32
32
  import { getConsentDismissScript, getCacheSelectorScript, getTermsAutoCheckScript } from "./consent-dismiss.js";
33
33
  import { solveCaptcha, isCaptchaSolverEnabled } from "./captcha-solver.js";
@@ -62,6 +62,47 @@ const LEAP_HUD = process.env.LEAP_HUD === "true";
62
62
  const LEAP_AUTO_CONSENT = process.env.LEAP_AUTO_CONSENT !== "false"; // default ON
63
63
  const LEAP_TRACE = process.env.LEAP_TRACE === "true";
64
64
  const LEAP_RECORD = process.env.LEAP_RECORD === "true";
65
+ const LEAP_AD_BLOCK = process.env.LEAP_AD_BLOCK !== "false"; // default ON
66
+ // ── Ad/Tracker Blocking ──────────────────────────────────────────────────
67
+ const AD_BLOCK_DOMAINS = new Set([
68
+ "doubleclick.net", "googlesyndication.com", "googleadservices.com",
69
+ "google-analytics.com", "googletagmanager.com", "googletagservices.com",
70
+ "adservice.google.com", "pagead2.googlesyndication.com",
71
+ "facebook.net", "connect.facebook.net", "fbcdn.net",
72
+ "amazon-adsystem.com", "ads-api.twitter.com",
73
+ "ads.yahoo.com", "analytics.yahoo.com",
74
+ "scorecardresearch.com", "quantserve.com", "outbrain.com",
75
+ "taboola.com", "criteo.com", "criteo.net",
76
+ "moatads.com", "adsrvr.org", "adnxs.com", "rubiconproject.com",
77
+ "pubmatic.com", "openx.net", "casalemedia.com",
78
+ "chartbeat.com", "hotjar.com", "mixpanel.com", "segment.io",
79
+ "newrelic.com", "nr-data.net", "optimizely.com",
80
+ "demdex.net", "omtrdc.net", "2o7.net",
81
+ "tealiumiq.com", "tags.tiqcdn.com",
82
+ ]);
83
+ function shouldBlockUrl(url) {
84
+ try {
85
+ const hostname = new URL(url).hostname;
86
+ for (const domain of AD_BLOCK_DOMAINS) {
87
+ if (hostname === domain || hostname.endsWith("." + domain))
88
+ return true;
89
+ }
90
+ }
91
+ catch { }
92
+ return false;
93
+ }
94
+ function attachAdBlocker(context) {
95
+ if (!LEAP_AD_BLOCK)
96
+ return;
97
+ context.route("**/*", (route) => {
98
+ if (shouldBlockUrl(route.request().url())) {
99
+ route.abort("blockedbyclient").catch(() => { });
100
+ }
101
+ else {
102
+ route.fallback().catch(() => { });
103
+ }
104
+ });
105
+ }
65
106
  const sessions = new SessionManager({
66
107
  maxSessions: MAX_SESSIONS,
67
108
  idleTimeoutMs: IDLE_TIMEOUT_MS,
@@ -88,6 +129,9 @@ if (LEAP_TILE && LEAP_TILE !== "false") {
88
129
  const defaultW = LEAP_SCREEN_WIDTH > 0 ? LEAP_SCREEN_WIDTH : detectedScreen?.width ?? 1920;
89
130
  const defaultH = LEAP_SCREEN_HEIGHT > 0 ? LEAP_SCREEN_HEIGHT : detectedScreen?.height ?? 1080;
90
131
  tilesCoord = new TilesCoordinator(defaultW, defaultH);
132
+ // Purge ALL slots not owned by this process — handles zombie PIDs
133
+ // from /mcp reconnects where old node process lingers alive.
134
+ tilesCoord.purgeOtherPids().catch(() => { });
91
135
  // File watcher only needed for multi-terminal mode (multiple Leapfrog instances).
92
136
  // In single-instance mode, the watcher causes spurious reflows that fight
93
137
  // with external monitor positioning. Only enable when explicitly requested.
@@ -325,6 +369,9 @@ server.registerTool("session_create", {
325
369
  }
326
370
  // Always inject intervention detection (lightweight MutationObserver)
327
371
  await session.context.addInitScript(getDetectionInitScript());
372
+ // Agent eyes — cursor dot + scroll indicator (zero Node overhead, listens to native DOM events)
373
+ await session.context.addInitScript(getAgentEyesInitScript());
374
+ attachAdBlocker(session.context);
328
375
  // Start tracing if enabled
329
376
  if (LEAP_TRACE) {
330
377
  await session.context.tracing.start({ screenshots: true, snapshots: true });
@@ -382,6 +429,8 @@ server.registerTool("session_create_batch", {
382
429
  if (LEAP_AUTO_CONSENT)
383
430
  await session.context.addInitScript(getConsentDismissScript());
384
431
  await session.context.addInitScript(getDetectionInitScript());
432
+ await session.context.addInitScript(getAgentEyesInitScript());
433
+ attachAdBlocker(session.context);
385
434
  if (LEAP_TRACE)
386
435
  await session.context.tracing.start({ screenshots: true, snapshots: true });
387
436
  // Claim tile slot (without triggering reflow yet — watcher handles it)
@@ -418,7 +467,7 @@ server.registerTool("session_create_batch", {
418
467
  return;
419
468
  }
420
469
  await session.page.goto(spec.url, {
421
- waitUntil: spec.waitUntil || "load",
470
+ waitUntil: spec.waitUntil || "domcontentloaded",
422
471
  timeout: 30000,
423
472
  });
424
473
  results.push({ id: session.id, url: spec.url });
@@ -627,7 +676,7 @@ server.registerTool("navigate", {
627
676
  url: z.string().describe("Full URL including https://"),
628
677
  waitUntil: z
629
678
  .enum(["load", "domcontentloaded", "networkidle"])
630
- .default("load")
679
+ .default("domcontentloaded")
631
680
  .describe("Wait strategy. Use networkidle for SPAs."),
632
681
  autoRetry: z
633
682
  .boolean()
@@ -16,5 +16,11 @@ export declare function getClickRippleScript(x: number, y: number): string;
16
16
  */
17
17
  export declare function getScrollToTargetZoomIn(selector: string): string;
18
18
  export declare function getScrollToTargetZoomOut(selector: string): string;
19
+ /**
20
+ * Returns JS to inject cursor tracking + scroll indicator for headed sessions.
21
+ * Always-on for headed mode — not gated by LEAP_HUD.
22
+ * Zero Node.js overhead: listens to native DOM events dispatched by Playwright.
23
+ */
24
+ export declare function getAgentEyesInitScript(): string;
19
25
  /** Legacy single-call version (sync scroll only, no zoom). */
20
26
  export declare function getScrollToTargetScript(selector: string): string;
@@ -79,6 +79,7 @@ export function getHUDInitScript(sessionName) {
79
79
  container.appendChild(ripple);
80
80
  ripple.addEventListener('animationend', function() { ripple.remove(); });
81
81
  };
82
+
82
83
  })();`;
83
84
  }
84
85
  // ─── Live Update Scripts ───────────────────────────────────────────────────
@@ -124,6 +125,117 @@ export function getScrollToTargetZoomOut(selector) {
124
125
  }
125
126
  })()`;
126
127
  }
128
+ // ─── Agent Eyes Init Script ───────────────────────────────────────────────
129
+ /**
130
+ * Returns JS to inject cursor tracking + scroll indicator for headed sessions.
131
+ * Always-on for headed mode — not gated by LEAP_HUD.
132
+ * Zero Node.js overhead: listens to native DOM events dispatched by Playwright.
133
+ */
134
+ export function getAgentEyesInitScript() {
135
+ return `(function() {
136
+ if (window.__leapfrog_eyes_initialized) return;
137
+ window.__leapfrog_eyes_initialized = true;
138
+
139
+ function init() {
140
+ // ── CSS ──────────────────────────────────────────────────────────
141
+ var style = document.createElement('style');
142
+ style.setAttribute('data-leapfrog', 'true');
143
+ style.textContent = \`
144
+ #leapfrog-cursor {
145
+ position: fixed;
146
+ width: 20px;
147
+ height: 20px;
148
+ border-radius: 50%;
149
+ background: rgba(34, 197, 94, 0.8);
150
+ box-shadow: 0 0 12px rgba(34, 197, 94, 0.5), 0 0 4px rgba(34, 197, 94, 0.8);
151
+ pointer-events: none;
152
+ z-index: 2147483646;
153
+ transform: translate(-50%, -50%);
154
+ transition: left 0.04s linear, top 0.04s linear, opacity 0.3s ease;
155
+ opacity: 0;
156
+ }
157
+ #leapfrog-cursor-ring {
158
+ position: fixed;
159
+ width: 36px;
160
+ height: 36px;
161
+ border-radius: 50%;
162
+ border: 2px solid rgba(34, 197, 94, 0.4);
163
+ pointer-events: none;
164
+ z-index: 2147483646;
165
+ transform: translate(-50%, -50%);
166
+ transition: left 0.08s ease-out, top 0.08s ease-out, opacity 0.3s ease;
167
+ opacity: 0;
168
+ }
169
+ #leapfrog-scroll-indicator {
170
+ position: fixed;
171
+ right: 16px;
172
+ top: 50%;
173
+ width: 36px;
174
+ height: 36px;
175
+ border-radius: 50%;
176
+ background: rgba(34, 197, 94, 0.7);
177
+ color: #fff;
178
+ font-size: 20px;
179
+ line-height: 36px;
180
+ text-align: center;
181
+ pointer-events: none;
182
+ z-index: 2147483646;
183
+ opacity: 0;
184
+ transition: opacity 0.15s ease-out;
185
+ transform: translateY(-50%);
186
+ }
187
+ \`;
188
+ (document.head || document.documentElement).appendChild(style);
189
+
190
+ // ── Agent Cursor (dot + trailing ring) ──────────────────────────
191
+ var cursor = document.createElement('div');
192
+ cursor.id = 'leapfrog-cursor';
193
+ cursor.setAttribute('data-leapfrog', 'true');
194
+ (document.body || document.documentElement).appendChild(cursor);
195
+
196
+ var ring = document.createElement('div');
197
+ ring.id = 'leapfrog-cursor-ring';
198
+ ring.setAttribute('data-leapfrog', 'true');
199
+ (document.body || document.documentElement).appendChild(ring);
200
+
201
+ var cursorTimeout;
202
+ document.addEventListener('mousemove', function(e) {
203
+ cursor.style.left = e.clientX + 'px';
204
+ cursor.style.top = e.clientY + 'px';
205
+ cursor.style.opacity = '1';
206
+ ring.style.left = e.clientX + 'px';
207
+ ring.style.top = e.clientY + 'px';
208
+ ring.style.opacity = '1';
209
+ clearTimeout(cursorTimeout);
210
+ cursorTimeout = setTimeout(function() {
211
+ cursor.style.opacity = '0';
212
+ ring.style.opacity = '0';
213
+ }, 3000);
214
+ }, true);
215
+
216
+ // ── Scroll Indicator ────────────────────────────────────────────
217
+ var scrollArrow = document.createElement('div');
218
+ scrollArrow.id = 'leapfrog-scroll-indicator';
219
+ scrollArrow.setAttribute('data-leapfrog', 'true');
220
+ (document.body || document.documentElement).appendChild(scrollArrow);
221
+
222
+ var scrollFadeTimeout;
223
+ document.addEventListener('wheel', function(e) {
224
+ scrollArrow.textContent = e.deltaY > 0 ? '\\u25BC' : '\\u25B2';
225
+ scrollArrow.style.opacity = '1';
226
+ clearTimeout(scrollFadeTimeout);
227
+ scrollFadeTimeout = setTimeout(function() { scrollArrow.style.opacity = '0'; }, 400);
228
+ }, true);
229
+ }
230
+
231
+ // Defer until body exists — init scripts can run before DOM is ready
232
+ if (document.body) {
233
+ init();
234
+ } else {
235
+ document.addEventListener('DOMContentLoaded', init);
236
+ }
237
+ })();`;
238
+ }
127
239
  /** Legacy single-call version (sync scroll only, no zoom). */
128
240
  export function getScrollToTargetScript(selector) {
129
241
  const escaped = selector.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
@@ -87,6 +87,12 @@ export declare class TilesCoordinator {
87
87
  * @returns Session IDs of the reaped (dead) slots.
88
88
  */
89
89
  reapDeadSlots(): Promise<string[]>;
90
+ /**
91
+ * Purge all slots NOT owned by the current process.pid.
92
+ * Called on startup to clear ghost slots from previous instances
93
+ * that may still be alive (e.g., after /mcp reconnect).
94
+ */
95
+ purgeOtherPids(): Promise<number>;
90
96
  /**
91
97
  * Watch `tiles.json` for changes made by other instances.
92
98
  * The callback fires with the new state whenever the file changes.
@@ -283,6 +283,23 @@ export class TilesCoordinator {
283
283
  return deadIds;
284
284
  });
285
285
  }
286
+ /**
287
+ * Purge all slots NOT owned by the current process.pid.
288
+ * Called on startup to clear ghost slots from previous instances
289
+ * that may still be alive (e.g., after /mcp reconnect).
290
+ */
291
+ async purgeOtherPids() {
292
+ return withLock(async () => {
293
+ const state = readState(this.screenWidth, this.screenHeight);
294
+ const before = state.slots.length;
295
+ state.slots = state.slots.filter((s) => s.instancePid === process.pid);
296
+ if (state.slots.length !== before) {
297
+ recalculatePositions(state);
298
+ writeState(state);
299
+ }
300
+ return before - state.slots.length;
301
+ });
302
+ }
286
303
  /**
287
304
  * Watch `tiles.json` for changes made by other instances.
288
305
  * The callback fires with the new state whenever the file changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leapfrog-mcp",
3
- "version": "0.6.9",
3
+ "version": "0.7.1",
4
4
  "description": "Multi-session browser MCP for AI agents — 36 tools, stealth, persistent auth, code-first scripts, API sniffer, agent intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",