leapfrog-mcp 0.6.2 → 0.6.3

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 (57) hide show
  1. package/dist/adaptive-wait.d.ts +72 -0
  2. package/dist/adaptive-wait.js +695 -0
  3. package/dist/api-intelligence.d.ts +82 -0
  4. package/dist/api-intelligence.js +575 -0
  5. package/dist/captcha-solver.d.ts +26 -0
  6. package/dist/captcha-solver.js +547 -0
  7. package/dist/cdp-connector.d.ts +33 -0
  8. package/dist/cdp-connector.js +176 -0
  9. package/dist/consent-dismiss.d.ts +33 -0
  10. package/dist/consent-dismiss.js +358 -0
  11. package/dist/crash-recovery.d.ts +74 -0
  12. package/dist/crash-recovery.js +242 -0
  13. package/dist/domain-knowledge.d.ts +149 -0
  14. package/dist/domain-knowledge.js +449 -0
  15. package/dist/harness-intelligence.d.ts +65 -0
  16. package/dist/harness-intelligence.js +432 -0
  17. package/dist/humanize-fingerprint.d.ts +42 -0
  18. package/dist/humanize-fingerprint.js +161 -0
  19. package/dist/humanize-mouse.d.ts +95 -0
  20. package/dist/humanize-mouse.js +275 -0
  21. package/dist/humanize-pause.d.ts +48 -0
  22. package/dist/humanize-pause.js +111 -0
  23. package/dist/humanize-scroll.d.ts +67 -0
  24. package/dist/humanize-scroll.js +185 -0
  25. package/dist/humanize-typing.d.ts +60 -0
  26. package/dist/humanize-typing.js +258 -0
  27. package/dist/humanize-utils.d.ts +62 -0
  28. package/dist/humanize-utils.js +100 -0
  29. package/dist/intervention.d.ts +65 -0
  30. package/dist/intervention.js +591 -0
  31. package/dist/logger.d.ts +13 -0
  32. package/dist/logger.js +47 -0
  33. package/dist/network-intelligence.d.ts +70 -0
  34. package/dist/network-intelligence.js +424 -0
  35. package/dist/page-classifier.d.ts +33 -0
  36. package/dist/page-classifier.js +1000 -0
  37. package/dist/paginate.d.ts +42 -0
  38. package/dist/paginate.js +693 -0
  39. package/dist/recording.d.ts +72 -0
  40. package/dist/recording.js +934 -0
  41. package/dist/script-executor.d.ts +31 -0
  42. package/dist/script-executor.js +249 -0
  43. package/dist/session-hud.d.ts +20 -0
  44. package/dist/session-hud.js +134 -0
  45. package/dist/snapshot-differ.d.ts +26 -0
  46. package/dist/snapshot-differ.js +225 -0
  47. package/dist/ssrf.d.ts +28 -0
  48. package/dist/ssrf.js +290 -0
  49. package/dist/stealth-audit.d.ts +27 -0
  50. package/dist/stealth-audit.js +719 -0
  51. package/dist/stealth.d.ts +195 -0
  52. package/dist/stealth.js +1157 -0
  53. package/dist/tab-manager.d.ts +14 -0
  54. package/dist/tab-manager.js +306 -0
  55. package/dist/tiles-coordinator.d.ts +106 -0
  56. package/dist/tiles-coordinator.js +358 -0
  57. package/package.json +1 -1
@@ -0,0 +1,358 @@
1
+ // ─── Multi-Terminal Tiling Coordinator ─────────────────────────────────────
2
+ //
3
+ // File-based coordination for multiple Leapfrog MCP instances sharing a
4
+ // single screen. Each Claude Code terminal claims a tile slot; positions
5
+ // are recalculated whenever the grid changes.
6
+ //
7
+ // Shared state lives in ~/.leapfrog/tiles.json with a simple file lock
8
+ // (~/.leapfrog/tiles.lock) for atomic read-modify-write cycles.
9
+ //
10
+ // Zero npm dependencies — Node.js built-ins only (fs, path, os).
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import * as os from "node:os";
14
+ // ─── Constants ────────────────────────────────────────────────────────────
15
+ const LEAPFROG_DIR = path.join(os.homedir(), ".leapfrog");
16
+ const TILES_PATH = path.join(LEAPFROG_DIR, "tiles.json");
17
+ const LOCK_PATH = path.join(LEAPFROG_DIR, "tiles.lock");
18
+ /** Maximum attempts to acquire the file lock before giving up */
19
+ const LOCK_MAX_RETRIES = 50;
20
+ /** Base delay between lock retries (ms); jittered up to 1.5x */
21
+ const LOCK_RETRY_BASE_MS = 50;
22
+ /** Lock files older than this are assumed stale and removed */
23
+ const STALE_LOCK_THRESHOLD_MS = 30_000;
24
+ // ─── Internal Helpers ─────────────────────────────────────────────────────
25
+ /** Ensure ~/.leapfrog/ exists. */
26
+ function ensureDir() {
27
+ if (!fs.existsSync(LEAPFROG_DIR)) {
28
+ fs.mkdirSync(LEAPFROG_DIR, { recursive: true });
29
+ }
30
+ }
31
+ /**
32
+ * Calculate optimal grid dimensions for `count` tiles.
33
+ * Favours a column-heavy layout when the screen is wider than it is tall.
34
+ */
35
+ function calculateGrid(count, screenWidth, screenHeight) {
36
+ if (count <= 0) {
37
+ return { cols: 1, rows: 1, cellWidth: screenWidth, cellHeight: screenHeight };
38
+ }
39
+ const cols = Math.ceil(Math.sqrt(count * (screenWidth / screenHeight)));
40
+ const rows = Math.ceil(count / cols);
41
+ const cellWidth = Math.floor(screenWidth / cols);
42
+ const cellHeight = Math.floor(screenHeight / rows);
43
+ return { cols, rows, cellWidth, cellHeight };
44
+ }
45
+ /** Returns true when `pid` corresponds to a running process. */
46
+ function isPidAlive(pid) {
47
+ try {
48
+ process.kill(pid, 0);
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * If the lock file exists and is older than STALE_LOCK_THRESHOLD_MS,
57
+ * remove it so we don't deadlock on a crashed process.
58
+ */
59
+ function clearStaleLock() {
60
+ try {
61
+ const stat = fs.statSync(LOCK_PATH);
62
+ if (Date.now() - stat.mtimeMs > STALE_LOCK_THRESHOLD_MS) {
63
+ fs.unlinkSync(LOCK_PATH);
64
+ }
65
+ }
66
+ catch {
67
+ // Lock file doesn't exist or was already removed — fine.
68
+ }
69
+ }
70
+ /**
71
+ * Acquire an exclusive file lock, run `fn`, then release.
72
+ * Uses `wx` (exclusive create) on the lock file as a cross-platform mutex.
73
+ */
74
+ async function withLock(fn) {
75
+ ensureDir();
76
+ clearStaleLock();
77
+ let fd = null;
78
+ for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
79
+ try {
80
+ fd = fs.openSync(LOCK_PATH, "wx");
81
+ break;
82
+ }
83
+ catch {
84
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_BASE_MS + Math.random() * LOCK_RETRY_BASE_MS));
85
+ }
86
+ }
87
+ if (fd === null) {
88
+ throw new Error("Could not acquire tile lock after max retries");
89
+ }
90
+ try {
91
+ return await fn();
92
+ }
93
+ finally {
94
+ fs.closeSync(fd);
95
+ try {
96
+ fs.unlinkSync(LOCK_PATH);
97
+ }
98
+ catch {
99
+ // Already gone — race with another process is harmless here.
100
+ }
101
+ }
102
+ }
103
+ /** Read and parse tiles.json, returning a default empty state on any failure. */
104
+ function readState(screenWidth, screenHeight) {
105
+ try {
106
+ const raw = fs.readFileSync(TILES_PATH, "utf-8");
107
+ const parsed = JSON.parse(raw);
108
+ // Basic shape validation
109
+ if (!Array.isArray(parsed.slots) || typeof parsed.lastUpdated !== "number") {
110
+ throw new Error("corrupt");
111
+ }
112
+ return parsed;
113
+ }
114
+ catch {
115
+ return { slots: [], screenWidth, screenHeight, lastUpdated: Date.now() };
116
+ }
117
+ }
118
+ /** Write tiles.json atomically (write-to-tmp then rename). */
119
+ function writeState(state) {
120
+ ensureDir();
121
+ state.lastUpdated = Date.now();
122
+ const tmp = TILES_PATH + ".tmp";
123
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
124
+ fs.renameSync(tmp, TILES_PATH);
125
+ }
126
+ /**
127
+ * Assign (x, y, width, height) to every slot in-place based on current
128
+ * grid dimensions and slot ordering.
129
+ */
130
+ function recalculatePositions(state) {
131
+ const { cols, cellWidth, cellHeight } = calculateGrid(state.slots.length, state.screenWidth, state.screenHeight);
132
+ for (let i = 0; i < state.slots.length; i++) {
133
+ const col = i % cols;
134
+ const row = Math.floor(i / cols);
135
+ state.slots[i].x = col * cellWidth;
136
+ state.slots[i].y = row * cellHeight;
137
+ state.slots[i].width = cellWidth;
138
+ state.slots[i].height = cellHeight;
139
+ }
140
+ }
141
+ // ─── TilesCoordinator ─────────────────────────────────────────────────────
142
+ /**
143
+ * Coordinates screen real estate across multiple Leapfrog MCP instances
144
+ * running in separate terminals.
145
+ *
146
+ * All coordination happens through the filesystem — no sockets, no ports,
147
+ * no IPC. State is stored in `~/.leapfrog/tiles.json` and protected by
148
+ * a simple file lock (`~/.leapfrog/tiles.lock`).
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * const tiles = new TilesCoordinator(1920, 1080);
153
+ * const pos = await tiles.claimSlot("session-abc");
154
+ * // pos = { x: 0, y: 0, width: 960, height: 540 }
155
+ *
156
+ * // On shutdown:
157
+ * await tiles.releaseAll();
158
+ * ```
159
+ */
160
+ export class TilesCoordinator {
161
+ screenWidth;
162
+ screenHeight;
163
+ mySessions = new Set();
164
+ watcher = null;
165
+ /**
166
+ * Create a new TilesCoordinator.
167
+ * Creates `~/.leapfrog/` if it doesn't already exist.
168
+ *
169
+ * @param screenWidth Total screen width in pixels.
170
+ * @param screenHeight Total screen height in pixels.
171
+ */
172
+ constructor(screenWidth, screenHeight) {
173
+ this.screenWidth = screenWidth;
174
+ this.screenHeight = screenHeight;
175
+ ensureDir();
176
+ }
177
+ /**
178
+ * Update the screen dimensions used for grid calculations.
179
+ * Call this after detecting the real screen size (e.g., via CDP maximize).
180
+ * Triggers a recalculation of all slot positions in the shared state.
181
+ */
182
+ async updateScreenSize(width, height) {
183
+ if (width === this.screenWidth && height === this.screenHeight)
184
+ return;
185
+ this.screenWidth = width;
186
+ this.screenHeight = height;
187
+ return withLock(async () => {
188
+ const state = readState(this.screenWidth, this.screenHeight);
189
+ state.screenWidth = width;
190
+ state.screenHeight = height;
191
+ recalculatePositions(state);
192
+ writeState(state);
193
+ });
194
+ }
195
+ /**
196
+ * Claim a grid slot for the given session.
197
+ *
198
+ * Dead-PID slots are reaped first, then the new session is appended
199
+ * and all positions are recalculated to fill the grid evenly.
200
+ *
201
+ * @param sessionId Unique identifier for this session.
202
+ * @returns The assigned tile position and dimensions.
203
+ */
204
+ async claimSlot(sessionId) {
205
+ return withLock(async () => {
206
+ const state = readState(this.screenWidth, this.screenHeight);
207
+ // Update screen dimensions in case they changed
208
+ state.screenWidth = this.screenWidth;
209
+ state.screenHeight = this.screenHeight;
210
+ // Reap dead PIDs first
211
+ state.slots = state.slots.filter((s) => isPidAlive(s.instancePid));
212
+ // Remove any existing slot for this sessionId (re-claim scenario)
213
+ state.slots = state.slots.filter((s) => s.sessionId !== sessionId);
214
+ // Add the new slot (position is temporary — recalculated next)
215
+ state.slots.push({
216
+ sessionId,
217
+ instancePid: process.pid,
218
+ x: 0,
219
+ y: 0,
220
+ width: 0,
221
+ height: 0,
222
+ });
223
+ recalculatePositions(state);
224
+ writeState(state);
225
+ this.mySessions.add(sessionId);
226
+ const slot = state.slots.find((s) => s.sessionId === sessionId);
227
+ return { x: slot.x, y: slot.y, width: slot.width, height: slot.height };
228
+ });
229
+ }
230
+ /**
231
+ * Release a slot, removing it from the shared grid.
232
+ * Remaining slots are repositioned to fill the gap.
233
+ *
234
+ * @param sessionId The session to release.
235
+ */
236
+ async releaseSlot(sessionId) {
237
+ return withLock(async () => {
238
+ const state = readState(this.screenWidth, this.screenHeight);
239
+ state.slots = state.slots.filter((s) => s.sessionId !== sessionId);
240
+ recalculatePositions(state);
241
+ writeState(state);
242
+ this.mySessions.delete(sessionId);
243
+ });
244
+ }
245
+ /**
246
+ * Get the current grid layout including all live instances.
247
+ *
248
+ * @returns A snapshot of the shared tiling state.
249
+ */
250
+ async getLayout() {
251
+ return withLock(async () => {
252
+ const state = readState(this.screenWidth, this.screenHeight);
253
+ return state;
254
+ });
255
+ }
256
+ /**
257
+ * Remove slots whose owning process is no longer running.
258
+ * Called automatically on `claimSlot`, but can be invoked directly
259
+ * for periodic housekeeping.
260
+ *
261
+ * @returns Session IDs of the reaped (dead) slots.
262
+ */
263
+ async reapDeadSlots() {
264
+ return withLock(async () => {
265
+ const state = readState(this.screenWidth, this.screenHeight);
266
+ const before = state.slots.length;
267
+ const deadIds = [];
268
+ state.slots = state.slots.filter((s) => {
269
+ if (!isPidAlive(s.instancePid)) {
270
+ deadIds.push(s.sessionId);
271
+ return false;
272
+ }
273
+ return true;
274
+ });
275
+ if (state.slots.length !== before) {
276
+ recalculatePositions(state);
277
+ writeState(state);
278
+ }
279
+ // Clean up local tracking for reaped sessions that were ours
280
+ for (const id of deadIds) {
281
+ this.mySessions.delete(id);
282
+ }
283
+ return deadIds;
284
+ });
285
+ }
286
+ /**
287
+ * Watch `tiles.json` for changes made by other instances.
288
+ * The callback fires with the new state whenever the file changes.
289
+ *
290
+ * @param onChange Callback invoked with the updated tiling state.
291
+ */
292
+ watch(onChange) {
293
+ this.unwatch(); // Prevent duplicate watchers
294
+ // Ensure the file exists before watching
295
+ if (!fs.existsSync(TILES_PATH)) {
296
+ writeState({
297
+ slots: [],
298
+ screenWidth: this.screenWidth,
299
+ screenHeight: this.screenHeight,
300
+ lastUpdated: Date.now(),
301
+ });
302
+ }
303
+ let debounce = null;
304
+ let lastMtime = 0;
305
+ try {
306
+ lastMtime = fs.statSync(TILES_PATH).mtimeMs;
307
+ }
308
+ catch { /* ok */ }
309
+ // Watch the DIRECTORY, not the file. fs.watch on a file breaks when
310
+ // atomic writes (rename .tmp → tiles.json) replace the inode.
311
+ // Directory watchers see rename events reliably on all platforms.
312
+ this.watcher = fs.watch(LEAPFROG_DIR, (_event, filename) => {
313
+ if (filename !== "tiles.json")
314
+ return;
315
+ // Debounce rapid successive writes
316
+ if (debounce)
317
+ clearTimeout(debounce);
318
+ debounce = setTimeout(() => {
319
+ try {
320
+ // Skip if mtime hasn't actually changed (self-write echo)
321
+ const mtime = fs.statSync(TILES_PATH).mtimeMs;
322
+ if (mtime === lastMtime)
323
+ return;
324
+ lastMtime = mtime;
325
+ const state = readState(this.screenWidth, this.screenHeight);
326
+ onChange(state);
327
+ }
328
+ catch {
329
+ // File may be mid-write; next event will pick it up.
330
+ }
331
+ }, 100);
332
+ });
333
+ // Don't let the watcher keep the process alive
334
+ this.watcher.unref();
335
+ }
336
+ /**
337
+ * Stop watching for tile changes from other instances.
338
+ */
339
+ unwatch() {
340
+ if (this.watcher) {
341
+ this.watcher.close();
342
+ this.watcher = null;
343
+ }
344
+ }
345
+ /**
346
+ * Release all slots owned by this process.
347
+ * Call this on SIGINT / SIGTERM to clean up before exit.
348
+ */
349
+ async releaseAll() {
350
+ return withLock(async () => {
351
+ const state = readState(this.screenWidth, this.screenHeight);
352
+ state.slots = state.slots.filter((s) => !this.mySessions.has(s.sessionId));
353
+ recalculatePositions(state);
354
+ writeState(state);
355
+ this.mySessions.clear();
356
+ });
357
+ }
358
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leapfrog-mcp",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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",