@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/lib/cdp.js CHANGED
@@ -28,15 +28,19 @@
28
28
  const { formatLogMessage } = require('./colorize');
29
29
 
30
30
  /**
31
- * Creates a reusable timeout promise to reduce function allocation overhead
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} Promise that rejects after timeout
36
+ * @returns {Promise} Resolves/rejects with the operation result, or rejects on timeout
35
37
  */
36
- function createTimeoutPromise(ms, message) {
37
- return new Promise((_, reject) =>
38
- setTimeout(() => reject(new Error(message)), ms)
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 Promise.race([
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 Promise.race([
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 Promise.race([
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 Promise.race([
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 createEnhancedCDPSession
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
- // Normalize the rule by removing different prefixes/formats
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
- let normalized = rule;
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
  /**
@@ -148,14 +148,16 @@ class DomainCache {
148
148
  */
149
149
  clearOldestEntries(count) {
150
150
  if (count <= 0) return;
151
-
152
- const entries = Array.from(this.cache);
153
- const toRemove = entries.slice(0, count);
154
-
155
- toRemove.forEach(domain => this.cache.delete(domain));
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 ${toRemove.length} old entries, cache size now: ${this.cache.size}`));
160
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cleared ${removed} old entries, cache size now: ${this.cache.size}`));
159
161
  }
160
162
  }
161
163
 
@@ -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, {