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,242 @@
1
+ // ─── Browser Crash Recovery Handler ───────────────────────────────────────
2
+ //
3
+ // Monitors browser health, provides session-level health checks, handles
4
+ // page.on('crash') events, auto-recovery, and crash telemetry.
5
+ // Standalone module — uses import type only for Playwright types.
6
+ // Logger integration is left to the caller (no cross-dependency).
7
+ const HEALTH_CHECK_TIMEOUT_MS = 3000;
8
+ export class CrashRecovery {
9
+ /** Crash telemetry — all page crashes during server lifetime */
10
+ crashLog = [];
11
+ /** Sessions marked as unhealthy after a page crash (pending recovery) */
12
+ unhealthySessions = new Set();
13
+ /**
14
+ * Attach a disconnect handler to a Playwright Browser instance.
15
+ * On unexpected disconnect (crash, kill, OOM), logs the event and
16
+ * calls onCrash so the SessionManager can clear its state.
17
+ */
18
+ attachToBrowser(browser, onCrash) {
19
+ browser.on("disconnected", () => {
20
+ // Write directly to stderr — no logger dependency
21
+ const entry = {
22
+ ts: new Date().toISOString(),
23
+ level: "error",
24
+ event: "browser.crashed",
25
+ pid: process.pid,
26
+ message: "Browser disconnected unexpectedly. Clearing all sessions.",
27
+ };
28
+ process.stderr.write(JSON.stringify(entry) + "\n");
29
+ onCrash();
30
+ });
31
+ }
32
+ /**
33
+ * Attach a page.on('crash') handler to catch Akamai-style browser context
34
+ * crashes. On crash: close the dead page, clean up the pool slot, log the
35
+ * event with the URL that caused the crash, and mark the session unhealthy.
36
+ *
37
+ * @param page The Playwright page to monitor
38
+ * @param session The session owning this page
39
+ * @param onCrash Optional callback when crash is detected (e.g., for cleanup)
40
+ */
41
+ attachToPage(page, session, onCrash) {
42
+ page.on("crash", () => {
43
+ let url = "";
44
+ try {
45
+ url = page.url();
46
+ }
47
+ catch {
48
+ // Page may be too dead to get URL
49
+ url = "(unknown — page unresponsive)";
50
+ }
51
+ const crashEntry = {
52
+ sessionId: session.id,
53
+ url,
54
+ timestamp: Date.now(),
55
+ error: "Page crashed (renderer process died)",
56
+ };
57
+ this.crashLog.push(crashEntry);
58
+ this.unhealthySessions.add(session.id);
59
+ // Write directly to stderr — no logger dependency
60
+ const logEntry = {
61
+ ts: new Date().toISOString(),
62
+ level: "error",
63
+ event: "page.crashed",
64
+ pid: process.pid,
65
+ sessionId: session.id,
66
+ url,
67
+ message: "Page renderer crashed. Session marked unhealthy for auto-recovery.",
68
+ };
69
+ process.stderr.write(JSON.stringify(logEntry) + "\n");
70
+ // Try to close the dead page (best-effort)
71
+ page.close().catch(() => { });
72
+ if (onCrash) {
73
+ onCrash(session.id);
74
+ }
75
+ });
76
+ }
77
+ /**
78
+ * Check if a session is marked as unhealthy (crashed but not yet recovered).
79
+ */
80
+ isUnhealthy(sessionId) {
81
+ return this.unhealthySessions.has(sessionId);
82
+ }
83
+ /**
84
+ * Mark a session as recovered after auto-recovery creates a replacement page.
85
+ */
86
+ markRecovered(sessionId) {
87
+ this.unhealthySessions.delete(sessionId);
88
+ }
89
+ /**
90
+ * Auto-recover a crashed session by creating a replacement page in the same
91
+ * browser context and re-applying stealth init scripts.
92
+ *
93
+ * @param session The session to recover
94
+ * @param applyStealthFn Callback to re-apply stealth to the new page
95
+ * @param rewireNetworkFn Callback to re-wire network intelligence
96
+ * @returns The new replacement page, or null if recovery failed
97
+ */
98
+ async autoRecover(session, applyStealthFn, rewireNetworkFn) {
99
+ if (!this.unhealthySessions.has(session.id)) {
100
+ return null; // Not crashed — nothing to recover
101
+ }
102
+ try {
103
+ const newPage = await session.context.newPage();
104
+ // Re-apply stealth init scripts
105
+ if (applyStealthFn) {
106
+ await applyStealthFn(newPage);
107
+ }
108
+ // Re-wire network intelligence
109
+ if (rewireNetworkFn) {
110
+ rewireNetworkFn(newPage, session);
111
+ }
112
+ // Auto-dismiss dialogs on replacement page
113
+ newPage.on("dialog", (dialog) => {
114
+ dialog.dismiss().catch(() => { });
115
+ });
116
+ // Replace the crashed page reference in the session
117
+ session.page = newPage;
118
+ // Update pages array if tab manager initialized it
119
+ if (session.pages) {
120
+ // Find and replace the crashed page, or push new one
121
+ const closedIdx = session.pages.findIndex((p) => {
122
+ try {
123
+ return p.isClosed();
124
+ }
125
+ catch {
126
+ return true;
127
+ }
128
+ });
129
+ if (closedIdx >= 0) {
130
+ session.pages[closedIdx] = newPage;
131
+ }
132
+ else {
133
+ session.pages.push(newPage);
134
+ }
135
+ }
136
+ // Attach crash handler to the new page too
137
+ this.attachToPage(newPage, session);
138
+ // Mark as recovered
139
+ this.unhealthySessions.delete(session.id);
140
+ const logEntry = {
141
+ ts: new Date().toISOString(),
142
+ level: "info",
143
+ event: "page.crash_recovered",
144
+ pid: process.pid,
145
+ sessionId: session.id,
146
+ message: "Replacement page created after crash. Session recovered.",
147
+ };
148
+ process.stderr.write(JSON.stringify(logEntry) + "\n");
149
+ return newPage;
150
+ }
151
+ catch (err) {
152
+ const error = err instanceof Error ? err.message : String(err);
153
+ const crashEntry = {
154
+ sessionId: session.id,
155
+ url: "(recovery failed)",
156
+ timestamp: Date.now(),
157
+ error: `Auto-recovery failed: ${error}`,
158
+ };
159
+ this.crashLog.push(crashEntry);
160
+ const logEntry = {
161
+ ts: new Date().toISOString(),
162
+ level: "error",
163
+ event: "page.crash_recovery_failed",
164
+ pid: process.pid,
165
+ sessionId: session.id,
166
+ error,
167
+ message: "Failed to create replacement page. Session is dead.",
168
+ };
169
+ process.stderr.write(JSON.stringify(logEntry) + "\n");
170
+ return null;
171
+ }
172
+ }
173
+ /**
174
+ * Quick health check for a single session.
175
+ * Verifies the page is not closed and can still evaluate JavaScript.
176
+ * Never throws — always returns a result object.
177
+ */
178
+ async healthCheck(session) {
179
+ // Check if session is marked unhealthy from a crash
180
+ if (this.unhealthySessions.has(session.id)) {
181
+ return { healthy: false, reason: "Page crashed — pending auto-recovery" };
182
+ }
183
+ try {
184
+ // Check if the page handle is already closed
185
+ if (session.page.isClosed()) {
186
+ return { healthy: false, reason: "Page is closed" };
187
+ }
188
+ // Try a trivial evaluate with a tight timeout
189
+ await Promise.race([
190
+ session.page.evaluate("1"),
191
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Health check timed out")), HEALTH_CHECK_TIMEOUT_MS)),
192
+ ]);
193
+ return { healthy: true };
194
+ }
195
+ catch (e) {
196
+ const reason = e instanceof Error ? e.message : "Unknown health check failure";
197
+ return { healthy: false, reason };
198
+ }
199
+ }
200
+ /**
201
+ * Run health checks on all sessions in parallel.
202
+ * Returns a Map from session ID to health result.
203
+ */
204
+ async healthCheckAll(sessions) {
205
+ const results = new Map();
206
+ if (sessions.size === 0)
207
+ return results;
208
+ const entries = Array.from(sessions.entries());
209
+ const checks = await Promise.allSettled(entries.map(([_id, session]) => this.healthCheck(session)));
210
+ for (let i = 0; i < entries.length; i++) {
211
+ const [id] = entries[i];
212
+ const outcome = checks[i];
213
+ if (outcome.status === "fulfilled") {
214
+ results.set(id, outcome.value);
215
+ }
216
+ else {
217
+ // Promise.allSettled rejection — should not happen since healthCheck never throws,
218
+ // but handle defensively
219
+ results.set(id, {
220
+ healthy: false,
221
+ reason: outcome.reason?.message ?? "Health check promise rejected",
222
+ });
223
+ }
224
+ }
225
+ return results;
226
+ }
227
+ /**
228
+ * Get the crash log — all page crashes during the current server lifetime.
229
+ * Returns an array of {sessionId, url, timestamp, error} entries.
230
+ */
231
+ getCrashLog() {
232
+ return [...this.crashLog];
233
+ }
234
+ /**
235
+ * Clear crash log (for testing or after log export).
236
+ */
237
+ clearCrashLog() {
238
+ this.crashLog = [];
239
+ }
240
+ }
241
+ export const crashRecovery = new CrashRecovery();
242
+ export default crashRecovery;
@@ -0,0 +1,149 @@
1
+ export interface DomainRecord {
2
+ domain: string;
3
+ stealthTier: number;
4
+ blockHistory: Array<{
5
+ timestamp: number;
6
+ reason: string;
7
+ }>;
8
+ waitStrategy: {
9
+ method: string;
10
+ avgLoadTime: number;
11
+ samples: number;
12
+ } | null;
13
+ rateLimit: {
14
+ minDelayMs: number;
15
+ lastAdjusted: number;
16
+ } | null;
17
+ consentSelector: string | null;
18
+ challengeResolution: {
19
+ method: string;
20
+ successCount: number;
21
+ lastSuccess: number;
22
+ } | null;
23
+ stableElements: Array<{
24
+ fingerprint: string;
25
+ seenCount: number;
26
+ firstSeen: number;
27
+ }>;
28
+ /** Consecutive visits where a tracked fingerprint was absent. Reset on re-appearance. */
29
+ stableElementMisses?: Record<string, number>;
30
+ apiEndpoints: Array<{
31
+ path: string;
32
+ method: string;
33
+ classification: string;
34
+ lastSeen: number;
35
+ }>;
36
+ elementMemory: Array<{
37
+ fingerprint: string;
38
+ lastSelector: string;
39
+ lastSeen: number;
40
+ }>;
41
+ banditState?: {
42
+ weights: number[];
43
+ gamma: number;
44
+ numArms: number;
45
+ };
46
+ visitCount: number;
47
+ firstVisit: number;
48
+ lastVisit: number;
49
+ }
50
+ export interface NavigationHints {
51
+ waitUntil?: string;
52
+ stealthTier?: number;
53
+ consentSelector?: string | null;
54
+ /** Whether the domain has a persisted bandit state to restore. */
55
+ hasBanditState?: boolean;
56
+ }
57
+ /**
58
+ * Normalize a domain string for consistent storage.
59
+ * Strips `www.` prefix and lowercases so that `www.wikipedia.org`
60
+ * and `Wikipedia.org` resolve to the same record.
61
+ */
62
+ export declare function normalizeDomain(domain: string): string;
63
+ export declare class DomainKnowledge {
64
+ private cache;
65
+ private dirty;
66
+ private readonly baseDir;
67
+ private readonly maxDomains;
68
+ private dirEnsured;
69
+ constructor(baseDir?: string, maxDomains?: number);
70
+ /** Load domain record from cache or disk. Creates empty if not found. */
71
+ load(domain: string): Promise<DomainRecord>;
72
+ /** Get from cache only (sync, for hot paths). */
73
+ get(domain: string): DomainRecord | undefined;
74
+ /** Merge partial updates into a domain record. Marks dirty. */
75
+ update(domain: string, partial: Partial<DomainRecord>): void;
76
+ /** Record a successful navigation — updates waitStrategy running average. */
77
+ recordNavigation(domain: string, method: string, durationMs: number): void;
78
+ /** Record a block event — escalates stealth tier if 2+ blocks in the last hour. */
79
+ recordBlock(domain: string, reason: string): void;
80
+ /** Record a consent dismissal selector for future visits. */
81
+ recordConsent(domain: string, selector: string): void;
82
+ /** Record a successful challenge resolution method for future visits. */
83
+ recordChallengeResolution(domain: string, method: string): void;
84
+ /** Get the known-good challenge resolution method for a domain. */
85
+ getLearnedChallengeMethod(domain: string): string | null;
86
+ /** Record discovered API endpoints, merging with existing. */
87
+ recordApiEndpoints(domain: string, endpoints: Array<{
88
+ path: string;
89
+ method: string;
90
+ classification: string;
91
+ }>): void;
92
+ /**
93
+ * Record element fingerprints from a snapshot visit.
94
+ * Increments seenCount for known fingerprints, adds new ones,
95
+ * and removes fingerprints not seen in 3+ consecutive visits.
96
+ */
97
+ recordElementFingerprints(domain: string, fingerprints: string[]): void;
98
+ /**
99
+ * Get fingerprints of stable elements (seenCount >= minSeen) for suppression.
100
+ * Returns them sorted by seenCount descending (highest confidence first).
101
+ */
102
+ getStableFingerprints(domain: string, minSeen?: number): string[];
103
+ private static readonly ELEMENT_MEMORY_CAP;
104
+ /** Upsert an element fingerprint → selector mapping into domain memory. */
105
+ recordElement(domain: string, fingerprint: string, selector: string): void;
106
+ /** Look up the last known selector for an element fingerprint on a domain. */
107
+ findElement(domain: string, fingerprint: string): string | undefined;
108
+ /** Flush all dirty records to disk. */
109
+ flush(): Promise<void>;
110
+ /** Flush a single domain to disk. */
111
+ flushDomain(domain: string): Promise<void>;
112
+ /** List all loaded domains (for MCP tool display). */
113
+ listDomains(): Array<{
114
+ domain: string;
115
+ visitCount: number;
116
+ lastVisit: number;
117
+ stealthTier: number;
118
+ }>;
119
+ /** Get full record for display. Loads from disk if needed. */
120
+ inspect(domain: string): Promise<DomainRecord | null>;
121
+ /**
122
+ * Return actionable navigation hints from stored domain knowledge.
123
+ * This is the read-side of the self-improvement loop: past visits
124
+ * inform future navigation strategy, stealth tier, and consent handling.
125
+ *
126
+ * Returns an empty object if no useful knowledge exists yet.
127
+ */
128
+ getNavigationHints(domain: string): Promise<NavigationHints>;
129
+ /** Persist bandit state into a domain record and mark dirty. */
130
+ saveBanditState(domain: string, state: {
131
+ weights: number[];
132
+ gamma: number;
133
+ numArms: number;
134
+ }): void;
135
+ /** Get the persisted bandit state for a domain (from cache). */
136
+ getBanditState(domain: string): {
137
+ weights: number[];
138
+ gamma: number;
139
+ numArms: number;
140
+ } | undefined;
141
+ private createEmpty;
142
+ /** Ensure the storage directory exists (once per instance). */
143
+ private ensureDir;
144
+ /** Write a single domain record to disk. */
145
+ private writeDomain;
146
+ /** LRU eviction — remove least-recently-visited domains when over cap. */
147
+ private evict;
148
+ }
149
+ export declare const domainKnowledge: DomainKnowledge;