@fanboynz/network-scanner 2.0.58 → 2.0.60
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/CHANGELOG.md +970 -0
- package/README.md +31 -0
- package/lib/adblock.js +215 -179
- package/lib/cdp.js +17 -169
- package/lib/compare.js +19 -32
- package/lib/domain-cache.js +9 -7
- package/lib/fingerprint.js +9 -1
- package/lib/ghost-cursor.js +258 -0
- package/lib/grep.js +9 -13
- package/lib/nettools.js +177 -42
- package/lib/output.js +17 -30
- package/nwss.js +214 -73
- package/package.json +5 -1
package/lib/cdp.js
CHANGED
|
@@ -28,15 +28,19 @@
|
|
|
28
28
|
const { formatLogMessage } = require('./colorize');
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Race a promise against a timeout, clearing the timer when the promise settles.
|
|
32
|
+
* Prevents leaked setTimeout handles that hold closure references until they fire.
|
|
33
|
+
* @param {Promise} promise - The operation to race
|
|
32
34
|
* @param {number} ms - Timeout in milliseconds
|
|
33
35
|
* @param {string} message - Error message for timeout
|
|
34
|
-
* @returns {Promise}
|
|
36
|
+
* @returns {Promise} Resolves/rejects with the operation result, or rejects on timeout
|
|
35
37
|
*/
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
function raceWithTimeout(promise, ms, message) {
|
|
39
|
+
let timeoutId;
|
|
40
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
41
|
+
timeoutId = setTimeout(() => reject(new Error(message)), ms);
|
|
42
|
+
});
|
|
43
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/**
|
|
@@ -59,10 +63,7 @@ const createSessionResult = (session = null, cleanup = async () => {}, isEnhance
|
|
|
59
63
|
* @returns {Promise<import('puppeteer').Page>} Page instance
|
|
60
64
|
*/
|
|
61
65
|
async function createPageWithTimeout(browser, timeout = 30000) {
|
|
62
|
-
return
|
|
63
|
-
browser.newPage(),
|
|
64
|
-
createTimeoutPromise(timeout, 'Page creation timeout - browser may be unresponsive')
|
|
65
|
-
]);
|
|
66
|
+
return raceWithTimeout(browser.newPage(), timeout, 'Page creation timeout - browser may be unresponsive');
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/**
|
|
@@ -73,24 +74,18 @@ async function createPageWithTimeout(browser, timeout = 30000) {
|
|
|
73
74
|
*/
|
|
74
75
|
async function setRequestInterceptionWithTimeout(page, timeout = 15000) {
|
|
75
76
|
try {
|
|
76
|
-
await
|
|
77
|
-
page.setRequestInterception(true),
|
|
78
|
-
createTimeoutPromise(timeout, 'Request interception timeout - first attempt')
|
|
79
|
-
]);
|
|
77
|
+
await raceWithTimeout(page.setRequestInterception(true), timeout, 'Request interception timeout - first attempt');
|
|
80
78
|
} catch (firstError) {
|
|
81
79
|
// Check for immediate critical failures
|
|
82
|
-
if (firstError.message.includes('Target closed') ||
|
|
80
|
+
if (firstError.message.includes('Target closed') ||
|
|
83
81
|
firstError.message.includes('Session closed') ||
|
|
84
82
|
firstError.message.includes('Browser has been closed')) {
|
|
85
83
|
throw new Error('CRITICAL_BROWSER_ERROR: ' + firstError.message);
|
|
86
84
|
}
|
|
87
|
-
|
|
85
|
+
|
|
88
86
|
// Retry with extended timeout
|
|
89
87
|
try {
|
|
90
|
-
await
|
|
91
|
-
page.setRequestInterception(true),
|
|
92
|
-
createTimeoutPromise(timeout * 2, 'Request interception timeout - retry failed')
|
|
93
|
-
]);
|
|
88
|
+
await raceWithTimeout(page.setRequestInterception(true), timeout * 2, 'Request interception timeout - retry failed');
|
|
94
89
|
} catch (retryError) {
|
|
95
90
|
if (retryError.message.includes('Network.enable timed out') ||
|
|
96
91
|
retryError.message.includes('ProtocolError')) {
|
|
@@ -168,10 +163,7 @@ async function createCDPSession(page, currentUrl, options = {}) {
|
|
|
168
163
|
try {
|
|
169
164
|
// Create CDP session using modern Puppeteer 20+ API
|
|
170
165
|
// Add timeout protection for CDP session creation
|
|
171
|
-
cdpSession = await
|
|
172
|
-
page.createCDPSession(),
|
|
173
|
-
createTimeoutPromise(20000, 'CDP session creation timeout')
|
|
174
|
-
]);
|
|
166
|
+
cdpSession = await raceWithTimeout(page.createCDPSession(), 20000, 'CDP session creation timeout');
|
|
175
167
|
|
|
176
168
|
// Enable network domain - required for network event monitoring
|
|
177
169
|
await cdpSession.send('Network.enable');
|
|
@@ -255,150 +247,6 @@ async function createCDPSession(page, currentUrl, options = {}) {
|
|
|
255
247
|
}
|
|
256
248
|
}
|
|
257
249
|
|
|
258
|
-
/**
|
|
259
|
-
* Validates CDP availability and configuration
|
|
260
|
-
*
|
|
261
|
-
* USAGE IN YOUR APPLICATION:
|
|
262
|
-
* const validation = validateCDPConfig(siteConfig, globalCDPFlag);
|
|
263
|
-
* if (!validation.isValid) {
|
|
264
|
-
* console.warn('CDP configuration issues detected');
|
|
265
|
-
* }
|
|
266
|
-
* validation.recommendations.forEach(rec => console.log('Recommendation:', rec));
|
|
267
|
-
*
|
|
268
|
-
* @param {object} siteConfig - Site configuration object
|
|
269
|
-
* @param {boolean} globalCDP - Global CDP flag
|
|
270
|
-
* @param {Array} cdpSpecificDomains - Array of domains for cdp_specific feature
|
|
271
|
-
* @returns {object} Validation result with recommendations
|
|
272
|
-
*/
|
|
273
|
-
function validateCDPConfig(siteConfig, globalCDP, cdpSpecificDomains = []) {
|
|
274
|
-
const warnings = [];
|
|
275
|
-
const recommendations = [];
|
|
276
|
-
|
|
277
|
-
// Check for conflicting configurations
|
|
278
|
-
if (globalCDP && siteConfig.cdp === false) {
|
|
279
|
-
warnings.push('Site-specific CDP disabled but global CDP is enabled - global setting will override');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Validate cdp_specific configuration
|
|
283
|
-
if (siteConfig.cdp_specific) {
|
|
284
|
-
if (!Array.isArray(siteConfig.cdp_specific)) {
|
|
285
|
-
warnings.push('cdp_specific must be an array of domain strings');
|
|
286
|
-
} else if (siteConfig.cdp_specific.length === 0) {
|
|
287
|
-
warnings.push('cdp_specific is empty - no domains will have CDP enabled');
|
|
288
|
-
} else {
|
|
289
|
-
// Validate domain format
|
|
290
|
-
const hasInvalidDomains = siteConfig.cdp_specific.some(domain =>
|
|
291
|
-
typeof domain !== 'string' || domain.trim() === ''
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
if (hasInvalidDomains) {
|
|
295
|
-
// Only filter invalid domains if we need to show them
|
|
296
|
-
const invalidDomains = siteConfig.cdp_specific.filter(domain =>
|
|
297
|
-
typeof domain !== 'string' || domain.trim() === ''
|
|
298
|
-
);
|
|
299
|
-
warnings.push(`cdp_specific contains invalid domains: ${invalidDomains.join(', ')}`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Performance recommendations
|
|
305
|
-
const cdpEnabled = globalCDP || siteConfig.cdp === true ||
|
|
306
|
-
(Array.isArray(siteConfig.cdp_specific) && siteConfig.cdp_specific.length > 0);
|
|
307
|
-
|
|
308
|
-
if (cdpEnabled) {
|
|
309
|
-
recommendations.push('CDP logging enabled - this may impact performance for high-traffic sites');
|
|
310
|
-
|
|
311
|
-
if (siteConfig.timeout && siteConfig.timeout < 30000) {
|
|
312
|
-
recommendations.push('Consider increasing timeout when using CDP logging to avoid protocol timeouts');
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
isValid: true,
|
|
318
|
-
warnings,
|
|
319
|
-
recommendations
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Enhanced CDP session with additional network monitoring features
|
|
325
|
-
*
|
|
326
|
-
* ADVANCED FEATURES:
|
|
327
|
-
* - JavaScript exception monitoring
|
|
328
|
-
* - Security state change detection
|
|
329
|
-
* - Failed network request tracking
|
|
330
|
-
* - Enhanced error reporting
|
|
331
|
-
*
|
|
332
|
-
* USE CASES:
|
|
333
|
-
* - Security analysis requiring comprehensive monitoring
|
|
334
|
-
* - Debugging complex single-page applications
|
|
335
|
-
* - Performance analysis of web applications
|
|
336
|
-
* - Research requiring detailed browser insights
|
|
337
|
-
*
|
|
338
|
-
* PERFORMANCE IMPACT:
|
|
339
|
-
* - Adds additional CDP domain subscriptions
|
|
340
|
-
* - Higher memory usage due to more event listeners
|
|
341
|
-
* - Recommended only for detailed analysis scenarios
|
|
342
|
-
*
|
|
343
|
-
* @param {import('puppeteer').Page} page - The Puppeteer page instance
|
|
344
|
-
* @param {string} currentUrl - The URL being processed
|
|
345
|
-
* @param {object} options - Configuration options (same as createCDPSession)
|
|
346
|
-
* @returns {Promise<object>} Enhanced CDP session object with isEnhanced flag
|
|
347
|
-
*/
|
|
348
|
-
async function createEnhancedCDPSession(page, currentUrl, options = {}) {
|
|
349
|
-
const basicSession = await createCDPSession(page, currentUrl, options);
|
|
350
|
-
|
|
351
|
-
if (!basicSession.session) {
|
|
352
|
-
// Ensure enhanced flag is set even for null sessions
|
|
353
|
-
return { ...basicSession, isEnhanced: false };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const { session } = basicSession;
|
|
357
|
-
const { forceDebug } = options;
|
|
358
|
-
|
|
359
|
-
try {
|
|
360
|
-
// Enable additional CDP domains for enhanced monitoring
|
|
361
|
-
await session.send('Runtime.enable'); // For JavaScript exceptions
|
|
362
|
-
await session.send('Security.enable'); // For security state changes
|
|
363
|
-
|
|
364
|
-
// Monitor JavaScript exceptions - useful for debugging problematic sites
|
|
365
|
-
session.on('Runtime.exceptionThrown', (params) => {
|
|
366
|
-
if (forceDebug) {
|
|
367
|
-
console.log(formatLogMessage('debug', `[cdp][exception] ${params.exceptionDetails.text}`));
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// Monitor security state changes - detect mixed content, certificate issues, etc.
|
|
372
|
-
session.on('Security.securityStateChanged', (params) => {
|
|
373
|
-
if (forceDebug && params.securityState !== 'secure') {
|
|
374
|
-
console.log(formatLogMessage('debug', `[cdp][security] Security state: ${params.securityState}`));
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// Monitor failed network requests - useful for understanding site issues
|
|
379
|
-
session.on('Network.loadingFailed', (params) => {
|
|
380
|
-
if (forceDebug) {
|
|
381
|
-
console.log(formatLogMessage('debug', `[cdp][failed] ${params.errorText}: ${params.requestId}`));
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
return {
|
|
386
|
-
session,
|
|
387
|
-
cleanup: basicSession.cleanup,
|
|
388
|
-
isEnhanced: true // Flag to indicate enhanced features are active
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
} catch (enhancedErr) {
|
|
392
|
-
if (forceDebug) {
|
|
393
|
-
console.log(formatLogMessage('debug', `Enhanced CDP features failed, falling back to basic session: ${enhancedErr.message}`));
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Graceful degradation: return basic session if enhanced features fail
|
|
397
|
-
// This ensures your application continues working even if advanced features break
|
|
398
|
-
return { ...basicSession, isEnhanced: false };
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
250
|
// EXPORT INTERFACE FOR OTHER APPLICATIONS:
|
|
403
251
|
// This module provides a clean, reusable interface for CDP integration.
|
|
404
252
|
// Simply require this module and use the exported functions.
|
|
@@ -406,7 +254,7 @@ async function createEnhancedCDPSession(page, currentUrl, options = {}) {
|
|
|
406
254
|
// CUSTOMIZATION TIPS:
|
|
407
255
|
// 1. Replace './colorize' import with your own logging system
|
|
408
256
|
// 2. Modify the request logging format in the Network.requestWillBeSent handler
|
|
409
|
-
// 3. Add additional CDP domain subscriptions in
|
|
257
|
+
// 3. Add additional CDP domain subscriptions in createCDPSession
|
|
410
258
|
// 4. Customize error categorization in the catch blocks
|
|
411
259
|
//
|
|
412
260
|
// TROUBLESHOOTING:
|
package/lib/compare.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
+
// Pre-compiled regexes for rule normalization (shared across functions)
|
|
5
|
+
const RE_ADBLOCK_PREFIX = /^\|\|?/;
|
|
6
|
+
const RE_LOCALHOST_127 = /^127\.0\.0\.1\s+/;
|
|
7
|
+
const RE_LOCALHOST_000 = /^0\.0\.0\.0\s+/;
|
|
8
|
+
const RE_CARET_SUFFIX = /\^.*$/;
|
|
9
|
+
const RE_DOLLAR_SUFFIX = /\$.*$/;
|
|
10
|
+
|
|
11
|
+
function normalizeRuleInline(rule) {
|
|
12
|
+
return rule
|
|
13
|
+
.replace(RE_ADBLOCK_PREFIX, '')
|
|
14
|
+
.replace(RE_LOCALHOST_127, '')
|
|
15
|
+
.replace(RE_LOCALHOST_000, '')
|
|
16
|
+
.replace(RE_CARET_SUFFIX, '')
|
|
17
|
+
.replace(RE_DOLLAR_SUFFIX, '')
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
4
21
|
/**
|
|
5
22
|
* Loads rules from a comparison file and returns them as a Set for fast lookup
|
|
6
23
|
* @param {string} compareFilePath - Path to the file containing existing rules
|
|
@@ -17,23 +34,7 @@ function loadComparisonRules(compareFilePath, forceDebug = false) {
|
|
|
17
34
|
const rules = new Set();
|
|
18
35
|
|
|
19
36
|
for (const line of lines) {
|
|
20
|
-
|
|
21
|
-
let normalizedRule = line;
|
|
22
|
-
|
|
23
|
-
// Remove adblock prefixes (||, |, etc.)
|
|
24
|
-
normalizedRule = normalizedRule.replace(/^\|\|/, '');
|
|
25
|
-
normalizedRule = normalizedRule.replace(/^\|/, '');
|
|
26
|
-
|
|
27
|
-
// Remove localhost prefixes
|
|
28
|
-
normalizedRule = normalizedRule.replace(/^127\.0\.0\.1\s+/, '');
|
|
29
|
-
normalizedRule = normalizedRule.replace(/^0\.0\.0\.0\s+/, '');
|
|
30
|
-
|
|
31
|
-
// Remove adblock suffixes and modifiers
|
|
32
|
-
normalizedRule = normalizedRule.replace(/\^.*$/, ''); // Remove ^ and everything after
|
|
33
|
-
normalizedRule = normalizedRule.replace(/\$.*$/, ''); // Remove $ and everything after
|
|
34
|
-
|
|
35
|
-
// Clean up and add to set
|
|
36
|
-
normalizedRule = normalizedRule.trim();
|
|
37
|
+
const normalizedRule = normalizeRuleInline(line);
|
|
37
38
|
if (normalizedRule) {
|
|
38
39
|
rules.add(normalizedRule);
|
|
39
40
|
}
|
|
@@ -55,21 +56,7 @@ function loadComparisonRules(compareFilePath, forceDebug = false) {
|
|
|
55
56
|
* @returns {string} Normalized rule
|
|
56
57
|
*/
|
|
57
58
|
function normalizeRule(rule) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Remove adblock prefixes
|
|
61
|
-
normalized = normalized.replace(/^\|\|/, '');
|
|
62
|
-
normalized = normalized.replace(/^\|/, '');
|
|
63
|
-
|
|
64
|
-
// Remove localhost prefixes
|
|
65
|
-
normalized = normalized.replace(/^127\.0\.0\.1\s+/, '');
|
|
66
|
-
normalized = normalized.replace(/^0\.0\.0\.0\s+/, '');
|
|
67
|
-
|
|
68
|
-
// Remove adblock suffixes and modifiers
|
|
69
|
-
normalized = normalized.replace(/\^.*$/, '');
|
|
70
|
-
normalized = normalized.replace(/\$.*$/, '');
|
|
71
|
-
|
|
72
|
-
return normalized.trim();
|
|
59
|
+
return normalizeRuleInline(rule);
|
|
73
60
|
}
|
|
74
61
|
|
|
75
62
|
/**
|
package/lib/domain-cache.js
CHANGED
|
@@ -148,14 +148,16 @@ class DomainCache {
|
|
|
148
148
|
*/
|
|
149
149
|
clearOldestEntries(count) {
|
|
150
150
|
if (count <= 0) return;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
|
|
152
|
+
let removed = 0;
|
|
153
|
+
for (const domain of this.cache) {
|
|
154
|
+
if (removed >= count) break;
|
|
155
|
+
this.cache.delete(domain);
|
|
156
|
+
removed++;
|
|
157
|
+
}
|
|
158
|
+
|
|
157
159
|
if (this.enableLogging) {
|
|
158
|
-
console.log(formatLogMessage('debug', `${this.logPrefix} Cleared ${
|
|
160
|
+
console.log(formatLogMessage('debug', `${this.logPrefix} Cleared ${removed} old entries, cache size now: ${this.cache.size}`));
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
|
package/lib/fingerprint.js
CHANGED
|
@@ -1697,7 +1697,7 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1697
1697
|
const x = Math.floor(Math.random() * 800) + 100;
|
|
1698
1698
|
const y = Math.floor(Math.random() * 400) + 100;
|
|
1699
1699
|
window.dispatchEvent(new MouseEvent('mousemove', {
|
|
1700
|
-
clientX: x, clientY: y, bubbles: true, cancelable: true, view: window
|
|
1700
|
+
clientX: x, clientY: y, pageX: x, pageY: y, bubbles: true, cancelable: true, view: window
|
|
1701
1701
|
}));
|
|
1702
1702
|
window.dispatchEvent(new Event('scroll', { bubbles: true }));
|
|
1703
1703
|
document.dispatchEvent(new KeyboardEvent('keydown', {
|
|
@@ -2013,6 +2013,10 @@ async function simulateHumanBehavior(page, forceDebug) {
|
|
|
2013
2013
|
document.dispatchEvent(new MouseEvent('mousemove', {
|
|
2014
2014
|
clientX: mouseX,
|
|
2015
2015
|
clientY: mouseY,
|
|
2016
|
+
pageX: mouseX + (window.scrollX || 0),
|
|
2017
|
+
pageY: mouseY + (window.scrollY || 0),
|
|
2018
|
+
screenX: mouseX + (window.screenX || 0),
|
|
2019
|
+
screenY: mouseY + (window.screenY || 0),
|
|
2016
2020
|
bubbles: true,
|
|
2017
2021
|
cancelable: true,
|
|
2018
2022
|
view: window,
|
|
@@ -2026,6 +2030,10 @@ async function simulateHumanBehavior(page, forceDebug) {
|
|
|
2026
2030
|
document.dispatchEvent(new MouseEvent('click', {
|
|
2027
2031
|
clientX: mouseX,
|
|
2028
2032
|
clientY: mouseY,
|
|
2033
|
+
pageX: mouseX + (window.scrollX || 0),
|
|
2034
|
+
pageY: mouseY + (window.scrollY || 0),
|
|
2035
|
+
screenX: mouseX + (window.screenX || 0),
|
|
2036
|
+
screenY: mouseY + (window.screenY || 0),
|
|
2029
2037
|
bubbles: true,
|
|
2030
2038
|
cancelable: true,
|
|
2031
2039
|
view: window
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// === Ghost Cursor Module ===
|
|
2
|
+
// Optional wrapper around the ghost-cursor npm package for advanced Bezier-based mouse movements.
|
|
3
|
+
// Falls back gracefully to built-in interaction.js mouse if ghost-cursor is not installed.
|
|
4
|
+
//
|
|
5
|
+
// USAGE (JSON config):
|
|
6
|
+
// "cursor_mode": "ghost" Enable ghost-cursor for this site
|
|
7
|
+
// "ghost_cursor_speed": 1.5 Movement speed multiplier (default: 1.0)
|
|
8
|
+
// "ghost_cursor_hesitate": 100 Delay (ms) before clicking (default: 50)
|
|
9
|
+
// "ghost_cursor_overshoot": 500 Max overshoot distance in px (default: auto)
|
|
10
|
+
//
|
|
11
|
+
// USAGE (CLI):
|
|
12
|
+
// --ghost-cursor Enable ghost-cursor globally
|
|
13
|
+
//
|
|
14
|
+
// INSTALL:
|
|
15
|
+
// npm install ghost-cursor (optional dependency)
|
|
16
|
+
|
|
17
|
+
const { formatLogMessage } = require('./colorize');
|
|
18
|
+
|
|
19
|
+
let ghostCursorModule = null;
|
|
20
|
+
let ghostCursorAvailable = false;
|
|
21
|
+
|
|
22
|
+
// Attempt to load ghost-cursor at module init — optional dependency
|
|
23
|
+
try {
|
|
24
|
+
ghostCursorModule = require('ghost-cursor');
|
|
25
|
+
ghostCursorAvailable = true;
|
|
26
|
+
} catch {
|
|
27
|
+
// ghost-cursor not installed — this is fine, built-in mouse will be used
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if ghost-cursor is available
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function isGhostCursorAvailable() {
|
|
35
|
+
return ghostCursorAvailable;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a ghost-cursor instance bound to a Puppeteer page
|
|
40
|
+
* @param {import('puppeteer').Page} page - Puppeteer page instance
|
|
41
|
+
* @param {object} options - Configuration options
|
|
42
|
+
* @param {boolean} options.forceDebug - Enable debug logging
|
|
43
|
+
* @param {number} options.startX - Starting X coordinate (default: 0)
|
|
44
|
+
* @param {number} options.startY - Starting Y coordinate (default: 0)
|
|
45
|
+
* @returns {object|null} Ghost cursor instance or null if unavailable
|
|
46
|
+
*/
|
|
47
|
+
function createGhostCursor(page, options = {}) {
|
|
48
|
+
if (!ghostCursorAvailable) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { forceDebug, startX = 0, startY = 0 } = options;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const cursor = ghostCursorModule.createCursor(page, { x: startX, y: startY });
|
|
56
|
+
|
|
57
|
+
if (forceDebug) {
|
|
58
|
+
console.log(formatLogMessage('debug', '[ghost-cursor] Cursor instance created'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return cursor;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (forceDebug) {
|
|
64
|
+
console.log(formatLogMessage('debug', `[ghost-cursor] Failed to create cursor: ${err.message}`));
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Move cursor to coordinates using ghost-cursor Bezier paths
|
|
72
|
+
* Drop-in replacement for humanLikeMouseMove when ghost-cursor is active
|
|
73
|
+
*
|
|
74
|
+
* @param {object} cursor - Ghost cursor instance from createGhostCursor()
|
|
75
|
+
* @param {number} toX - Target X coordinate
|
|
76
|
+
* @param {number} toY - Target Y coordinate
|
|
77
|
+
* @param {object} options - Movement options
|
|
78
|
+
* @param {number} options.moveSpeed - Speed multiplier (default: auto/random)
|
|
79
|
+
* @param {number} options.moveDelay - Delay after movement in ms (default: 0)
|
|
80
|
+
* @param {boolean} options.randomizeMoveDelay - Randomize move delay (default: true)
|
|
81
|
+
* @param {number} options.overshootThreshold - Max overshoot distance in px
|
|
82
|
+
* @param {boolean} options.forceDebug - Enable debug logging
|
|
83
|
+
* @returns {Promise<boolean>} true if movement succeeded
|
|
84
|
+
*/
|
|
85
|
+
async function ghostMove(cursor, toX, toY, options = {}) {
|
|
86
|
+
if (!cursor) return false;
|
|
87
|
+
|
|
88
|
+
const {
|
|
89
|
+
moveSpeed,
|
|
90
|
+
moveDelay = 0,
|
|
91
|
+
randomizeMoveDelay = true,
|
|
92
|
+
overshootThreshold,
|
|
93
|
+
forceDebug
|
|
94
|
+
} = options;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const moveOpts = {};
|
|
98
|
+
if (moveSpeed !== undefined) moveOpts.moveSpeed = moveSpeed;
|
|
99
|
+
if (moveDelay > 0) moveOpts.moveDelay = moveDelay;
|
|
100
|
+
if (randomizeMoveDelay !== undefined) moveOpts.randomizeMoveDelay = randomizeMoveDelay;
|
|
101
|
+
if (overshootThreshold !== undefined) moveOpts.overshootThreshold = overshootThreshold;
|
|
102
|
+
|
|
103
|
+
await cursor.moveTo({ x: toX, y: toY }, moveOpts);
|
|
104
|
+
|
|
105
|
+
if (forceDebug) {
|
|
106
|
+
console.log(formatLogMessage('debug', `[ghost-cursor] Moved to (${Math.round(toX)}, ${Math.round(toY)})`));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (forceDebug) {
|
|
112
|
+
console.log(formatLogMessage('debug', `[ghost-cursor] Move failed: ${err.message}`));
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Click on a CSS selector or coordinates using ghost-cursor
|
|
120
|
+
*
|
|
121
|
+
* @param {object} cursor - Ghost cursor instance
|
|
122
|
+
* @param {string|{x: number, y: number}} target - CSS selector or {x, y} coordinates
|
|
123
|
+
* @param {object} options - Click options
|
|
124
|
+
* @param {number} options.hesitate - Delay (ms) before clicking (default: 50)
|
|
125
|
+
* @param {number} options.waitForClick - Delay (ms) between mousedown/mouseup (default: auto)
|
|
126
|
+
* @param {number} options.moveDelay - Delay (ms) after moving to target
|
|
127
|
+
* @param {number} options.paddingPercentage - Click point within element (0=edge, 100=center)
|
|
128
|
+
* @param {boolean} options.forceDebug - Enable debug logging
|
|
129
|
+
* @returns {Promise<boolean>} true if click succeeded
|
|
130
|
+
*/
|
|
131
|
+
async function ghostClick(cursor, target, options = {}) {
|
|
132
|
+
if (!cursor) return false;
|
|
133
|
+
|
|
134
|
+
const {
|
|
135
|
+
hesitate = 50,
|
|
136
|
+
waitForClick,
|
|
137
|
+
moveDelay,
|
|
138
|
+
paddingPercentage,
|
|
139
|
+
forceDebug
|
|
140
|
+
} = options;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const clickOpts = { hesitate };
|
|
144
|
+
if (waitForClick !== undefined) clickOpts.waitForClick = waitForClick;
|
|
145
|
+
if (moveDelay !== undefined) clickOpts.moveDelay = moveDelay;
|
|
146
|
+
if (paddingPercentage !== undefined) clickOpts.paddingPercentage = paddingPercentage;
|
|
147
|
+
|
|
148
|
+
if (typeof target === 'string') {
|
|
149
|
+
await cursor.click(target, clickOpts);
|
|
150
|
+
} else {
|
|
151
|
+
// For coordinate clicks, move first then use page click
|
|
152
|
+
await cursor.moveTo(target);
|
|
153
|
+
// Small hesitation before clicking
|
|
154
|
+
if (hesitate > 0) {
|
|
155
|
+
await new Promise(resolve => setTimeout(resolve, hesitate));
|
|
156
|
+
}
|
|
157
|
+
const page = cursor._page || cursor.page;
|
|
158
|
+
if (page && typeof page.mouse?.click === 'function') {
|
|
159
|
+
await page.mouse.click(target.x, target.y);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (forceDebug) {
|
|
164
|
+
const label = typeof target === 'string' ? target : `(${Math.round(target.x)}, ${Math.round(target.y)})`;
|
|
165
|
+
console.log(formatLogMessage('debug', `[ghost-cursor] Clicked ${label}`));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (forceDebug) {
|
|
171
|
+
console.log(formatLogMessage('debug', `[ghost-cursor] Click failed: ${err.message}`));
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Perform a random idle mouse movement using ghost-cursor
|
|
179
|
+
*
|
|
180
|
+
* @param {object} cursor - Ghost cursor instance
|
|
181
|
+
* @param {object} options - Options
|
|
182
|
+
* @param {boolean} options.forceDebug - Enable debug logging
|
|
183
|
+
* @returns {Promise<boolean>} true if movement succeeded
|
|
184
|
+
*/
|
|
185
|
+
async function ghostRandomMove(cursor, options = {}) {
|
|
186
|
+
if (!cursor) return false;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await cursor.randomMove();
|
|
190
|
+
if (options.forceDebug) {
|
|
191
|
+
console.log(formatLogMessage('debug', '[ghost-cursor] Random movement performed'));
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (options.forceDebug) {
|
|
196
|
+
console.log(formatLogMessage('debug', `[ghost-cursor] Random move failed: ${err.message}`));
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Generate a Bezier path between two points (standalone, no browser needed)
|
|
204
|
+
*
|
|
205
|
+
* @param {{x: number, y: number}} from - Start point
|
|
206
|
+
* @param {{x: number, y: number}} to - End point
|
|
207
|
+
* @param {object} options - Path options
|
|
208
|
+
* @param {number} options.spreadOverride - Override curve spread
|
|
209
|
+
* @param {number} options.moveSpeed - Movement speed
|
|
210
|
+
* @returns {Array<{x: number, y: number}>|null} Array of path points or null
|
|
211
|
+
*/
|
|
212
|
+
function ghostPath(from, to, options = {}) {
|
|
213
|
+
if (!ghostCursorAvailable || !ghostCursorModule.path) return null;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
return ghostCursorModule.path(from, to, options);
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resolve ghost-cursor settings from site config and CLI flags.
|
|
224
|
+
* Returns null if ghost-cursor should not be used.
|
|
225
|
+
*
|
|
226
|
+
* @param {object} siteConfig - Per-site JSON configuration
|
|
227
|
+
* @param {boolean} globalGhostCursor - CLI --ghost-cursor flag
|
|
228
|
+
* @param {boolean} forceDebug - Debug logging flag
|
|
229
|
+
* @returns {object|null} Resolved ghost-cursor options or null
|
|
230
|
+
*/
|
|
231
|
+
function resolveGhostCursorConfig(siteConfig, globalGhostCursor, forceDebug) {
|
|
232
|
+
const enabled = globalGhostCursor || siteConfig.cursor_mode === 'ghost';
|
|
233
|
+
|
|
234
|
+
if (!enabled) return null;
|
|
235
|
+
|
|
236
|
+
if (!ghostCursorAvailable) {
|
|
237
|
+
console.warn(formatLogMessage('warn', '[ghost-cursor] cursor_mode "ghost" requested but ghost-cursor package is not installed. Run: npm install ghost-cursor'));
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
moveSpeed: siteConfig.ghost_cursor_speed || undefined,
|
|
243
|
+
hesitate: siteConfig.ghost_cursor_hesitate ?? 50,
|
|
244
|
+
overshootThreshold: siteConfig.ghost_cursor_overshoot || undefined,
|
|
245
|
+
duration: siteConfig.ghost_cursor_duration || siteConfig.interact_duration || 2000,
|
|
246
|
+
forceDebug
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
isGhostCursorAvailable,
|
|
252
|
+
createGhostCursor,
|
|
253
|
+
ghostMove,
|
|
254
|
+
ghostClick,
|
|
255
|
+
ghostRandomMove,
|
|
256
|
+
ghostPath,
|
|
257
|
+
resolveGhostCursorConfig
|
|
258
|
+
};
|
package/lib/grep.js
CHANGED
|
@@ -41,23 +41,19 @@ function grepContent(content, searchPatterns, options = {}) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
try {
|
|
44
|
-
|
|
45
44
|
const allMatches = [];
|
|
46
45
|
let firstMatch = null;
|
|
47
|
-
|
|
46
|
+
|
|
47
|
+
// Build common args once outside the loop
|
|
48
|
+
const baseArgs = ['--text', '--color=never'];
|
|
49
|
+
if (ignoreCase) baseArgs.push('-i');
|
|
50
|
+
if (wholeWord) baseArgs.push('-w');
|
|
51
|
+
if (!regex) baseArgs.push('-F');
|
|
52
|
+
|
|
48
53
|
for (const pattern of searchPatterns) {
|
|
49
54
|
if (!pattern || pattern.trim().length === 0) continue;
|
|
50
|
-
|
|
51
|
-
const grepArgs = [
|
|
52
|
-
'--text', // Treat file as text
|
|
53
|
-
'--color=never', // Disable color output
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
if (ignoreCase) grepArgs.push('-i');
|
|
57
|
-
if (wholeWord) grepArgs.push('-w');
|
|
58
|
-
if (!regex) grepArgs.push('-F'); // Fixed strings (literal)
|
|
59
|
-
|
|
60
|
-
grepArgs.push(pattern);
|
|
55
|
+
|
|
56
|
+
const grepArgs = [...baseArgs, pattern];
|
|
61
57
|
|
|
62
58
|
try {
|
|
63
59
|
const result = spawnSync('grep', grepArgs, {
|