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.
- package/dist/adaptive-wait.d.ts +72 -0
- package/dist/adaptive-wait.js +695 -0
- package/dist/api-intelligence.d.ts +82 -0
- package/dist/api-intelligence.js +575 -0
- package/dist/captcha-solver.d.ts +26 -0
- package/dist/captcha-solver.js +547 -0
- package/dist/cdp-connector.d.ts +33 -0
- package/dist/cdp-connector.js +176 -0
- package/dist/consent-dismiss.d.ts +33 -0
- package/dist/consent-dismiss.js +358 -0
- package/dist/crash-recovery.d.ts +74 -0
- package/dist/crash-recovery.js +242 -0
- package/dist/domain-knowledge.d.ts +149 -0
- package/dist/domain-knowledge.js +449 -0
- package/dist/harness-intelligence.d.ts +65 -0
- package/dist/harness-intelligence.js +432 -0
- package/dist/humanize-fingerprint.d.ts +42 -0
- package/dist/humanize-fingerprint.js +161 -0
- package/dist/humanize-mouse.d.ts +95 -0
- package/dist/humanize-mouse.js +275 -0
- package/dist/humanize-pause.d.ts +48 -0
- package/dist/humanize-pause.js +111 -0
- package/dist/humanize-scroll.d.ts +67 -0
- package/dist/humanize-scroll.js +185 -0
- package/dist/humanize-typing.d.ts +60 -0
- package/dist/humanize-typing.js +258 -0
- package/dist/humanize-utils.d.ts +62 -0
- package/dist/humanize-utils.js +100 -0
- package/dist/intervention.d.ts +65 -0
- package/dist/intervention.js +591 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +47 -0
- package/dist/network-intelligence.d.ts +70 -0
- package/dist/network-intelligence.js +424 -0
- package/dist/page-classifier.d.ts +33 -0
- package/dist/page-classifier.js +1000 -0
- package/dist/paginate.d.ts +42 -0
- package/dist/paginate.js +693 -0
- package/dist/recording.d.ts +72 -0
- package/dist/recording.js +934 -0
- package/dist/script-executor.d.ts +31 -0
- package/dist/script-executor.js +249 -0
- package/dist/session-hud.d.ts +20 -0
- package/dist/session-hud.js +134 -0
- package/dist/snapshot-differ.d.ts +26 -0
- package/dist/snapshot-differ.js +225 -0
- package/dist/ssrf.d.ts +28 -0
- package/dist/ssrf.js +290 -0
- package/dist/stealth-audit.d.ts +27 -0
- package/dist/stealth-audit.js +719 -0
- package/dist/stealth.d.ts +195 -0
- package/dist/stealth.js +1157 -0
- package/dist/tab-manager.d.ts +14 -0
- package/dist/tab-manager.js +306 -0
- package/dist/tiles-coordinator.d.ts +106 -0
- package/dist/tiles-coordinator.js +358 -0
- 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.
|
|
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",
|