cdp-skill 1.0.2 → 1.0.4

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 (78) hide show
  1. package/README.md +3 -0
  2. package/SKILL.md +34 -5
  3. package/package.json +2 -1
  4. package/src/capture/console-capture.js +241 -0
  5. package/src/capture/debug-capture.js +144 -0
  6. package/src/capture/error-aggregator.js +151 -0
  7. package/src/capture/eval-serializer.js +320 -0
  8. package/src/capture/index.js +40 -0
  9. package/src/capture/network-capture.js +211 -0
  10. package/src/capture/pdf-capture.js +256 -0
  11. package/src/capture/screenshot-capture.js +325 -0
  12. package/src/cdp/browser.js +569 -0
  13. package/src/cdp/connection.js +369 -0
  14. package/src/cdp/discovery.js +138 -0
  15. package/src/cdp/index.js +29 -0
  16. package/src/cdp/target-and-session.js +439 -0
  17. package/src/cdp-skill.js +25 -11
  18. package/src/constants.js +79 -0
  19. package/src/dom/actionability.js +638 -0
  20. package/src/dom/click-executor.js +923 -0
  21. package/src/dom/element-handle.js +496 -0
  22. package/src/dom/element-locator.js +475 -0
  23. package/src/dom/element-validator.js +120 -0
  24. package/src/dom/fill-executor.js +489 -0
  25. package/src/dom/index.js +248 -0
  26. package/src/dom/input-emulator.js +406 -0
  27. package/src/dom/keyboard-executor.js +202 -0
  28. package/src/dom/quad-helpers.js +89 -0
  29. package/src/dom/react-filler.js +94 -0
  30. package/src/dom/wait-executor.js +423 -0
  31. package/src/index.js +6 -6
  32. package/src/page/cookie-manager.js +202 -0
  33. package/src/page/dom-stability.js +181 -0
  34. package/src/page/index.js +36 -0
  35. package/src/{page.js → page/page-controller.js} +109 -839
  36. package/src/page/wait-utilities.js +302 -0
  37. package/src/page/web-storage-manager.js +108 -0
  38. package/src/runner/context-helpers.js +224 -0
  39. package/src/runner/execute-browser.js +518 -0
  40. package/src/runner/execute-form.js +315 -0
  41. package/src/runner/execute-input.js +308 -0
  42. package/src/runner/execute-interaction.js +672 -0
  43. package/src/runner/execute-navigation.js +180 -0
  44. package/src/runner/execute-query.js +771 -0
  45. package/src/runner/index.js +51 -0
  46. package/src/runner/step-executors.js +421 -0
  47. package/src/runner/step-validator.js +641 -0
  48. package/src/tests/Actionability.test.js +613 -0
  49. package/src/tests/BrowserClient.test.js +1 -1
  50. package/src/tests/ChromeDiscovery.test.js +1 -1
  51. package/src/tests/ClickExecutor.test.js +554 -0
  52. package/src/tests/ConsoleCapture.test.js +1 -1
  53. package/src/tests/ContextHelpers.test.js +453 -0
  54. package/src/tests/CookieManager.test.js +450 -0
  55. package/src/tests/DebugCapture.test.js +307 -0
  56. package/src/tests/ElementHandle.test.js +1 -1
  57. package/src/tests/ElementLocator.test.js +1 -1
  58. package/src/tests/ErrorAggregator.test.js +1 -1
  59. package/src/tests/EvalSerializer.test.js +391 -0
  60. package/src/tests/FillExecutor.test.js +611 -0
  61. package/src/tests/InputEmulator.test.js +1 -1
  62. package/src/tests/KeyboardExecutor.test.js +430 -0
  63. package/src/tests/NetworkErrorCapture.test.js +1 -1
  64. package/src/tests/PageController.test.js +1 -1
  65. package/src/tests/PdfCapture.test.js +333 -0
  66. package/src/tests/ScreenshotCapture.test.js +1 -1
  67. package/src/tests/SessionRegistry.test.js +1 -1
  68. package/src/tests/StepValidator.test.js +527 -0
  69. package/src/tests/TargetManager.test.js +1 -1
  70. package/src/tests/TestRunner.test.js +1 -1
  71. package/src/tests/WaitStrategy.test.js +1 -1
  72. package/src/tests/WaitUtilities.test.js +508 -0
  73. package/src/tests/WebStorageManager.test.js +333 -0
  74. package/src/types.js +309 -0
  75. package/src/capture.js +0 -1400
  76. package/src/cdp.js +0 -1286
  77. package/src/dom.js +0 -4379
  78. package/src/runner.js +0 -3676
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Wait Executor
3
+ * Wait operations for selectors, text, visibility, and timing
4
+ *
5
+ * EXPORTS:
6
+ * - createWaitExecutor(session, elementLocator) → WaitExecutor
7
+ * Methods: execute, waitForSelector, waitForHidden, waitForCount,
8
+ * waitForText, waitForTextRegex, waitForUrlContains, waitForTime
9
+ *
10
+ * DEPENDENCIES:
11
+ * - ../constants.js: TIMEOUTS, POLL_INTERVALS
12
+ * - ../utils.js: sleep, timeoutError
13
+ */
14
+
15
+ import { TIMEOUTS, POLL_INTERVALS } from '../constants.js';
16
+ import { sleep, timeoutError } from '../utils.js';
17
+
18
+ const DEFAULT_TIMEOUT = TIMEOUTS.DEFAULT;
19
+ const MAX_TIMEOUT = TIMEOUTS.MAX;
20
+ const POLL_INTERVAL = POLL_INTERVALS.DEFAULT;
21
+
22
+ /**
23
+ * Create a wait executor for handling wait operations
24
+ * @param {Object} session - CDP session
25
+ * @param {Object} elementLocator - Element locator instance
26
+ * @returns {Object} Wait executor interface
27
+ */
28
+ export function createWaitExecutor(session, elementLocator) {
29
+ if (!session) throw new Error('CDP session is required');
30
+ if (!elementLocator) throw new Error('Element locator is required');
31
+
32
+ function validateTimeout(timeout) {
33
+ if (typeof timeout !== 'number' || !Number.isFinite(timeout)) {
34
+ return DEFAULT_TIMEOUT;
35
+ }
36
+ if (timeout < 0) return 0;
37
+ if (timeout > MAX_TIMEOUT) return MAX_TIMEOUT;
38
+ return timeout;
39
+ }
40
+
41
+ /**
42
+ * Wait for selector using browser-side MutationObserver
43
+ * Much faster than Node.js polling as it avoids network round-trips
44
+ */
45
+ async function waitForSelector(selector, timeout = DEFAULT_TIMEOUT) {
46
+ const validatedTimeout = validateTimeout(timeout);
47
+
48
+ try {
49
+ // Use browser-side polling with MutationObserver for better performance
50
+ const result = await session.send('Runtime.evaluate', {
51
+ expression: `
52
+ new Promise((resolve, reject) => {
53
+ const selector = ${JSON.stringify(selector)};
54
+ const timeout = ${validatedTimeout};
55
+
56
+ // Check if element already exists
57
+ const existing = document.querySelector(selector);
58
+ if (existing) {
59
+ resolve({ found: true, immediate: true });
60
+ return;
61
+ }
62
+
63
+ let resolved = false;
64
+ const timeoutId = setTimeout(() => {
65
+ if (!resolved) {
66
+ resolved = true;
67
+ observer.disconnect();
68
+ reject(new Error('Timeout waiting for selector: ' + selector));
69
+ }
70
+ }, timeout);
71
+
72
+ const observer = new MutationObserver((mutations, obs) => {
73
+ const el = document.querySelector(selector);
74
+ if (el && !resolved) {
75
+ resolved = true;
76
+ obs.disconnect();
77
+ clearTimeout(timeoutId);
78
+ resolve({ found: true, mutations: mutations.length });
79
+ }
80
+ });
81
+
82
+ observer.observe(document.documentElement || document.body, {
83
+ childList: true,
84
+ subtree: true,
85
+ attributes: true,
86
+ attributeFilter: ['class', 'id', 'style', 'hidden']
87
+ });
88
+
89
+ // Also check with RAF as a fallback
90
+ const checkWithRAF = () => {
91
+ if (resolved) return;
92
+ const el = document.querySelector(selector);
93
+ if (el) {
94
+ resolved = true;
95
+ observer.disconnect();
96
+ clearTimeout(timeoutId);
97
+ resolve({ found: true, raf: true });
98
+ return;
99
+ }
100
+ requestAnimationFrame(checkWithRAF);
101
+ };
102
+ requestAnimationFrame(checkWithRAF);
103
+ })
104
+ `,
105
+ awaitPromise: true,
106
+ returnByValue: true
107
+ });
108
+
109
+ if (result.exceptionDetails) {
110
+ throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text);
111
+ }
112
+
113
+ return result.result.value;
114
+ } catch (error) {
115
+ // Fall back to original Node.js polling if browser-side fails
116
+ const element = await elementLocator.waitForSelector(selector, {
117
+ timeout: validatedTimeout
118
+ });
119
+ if (element) await element.dispose();
120
+ }
121
+ }
122
+
123
+ async function checkElementHidden(selector) {
124
+ try {
125
+ const result = await session.send('Runtime.evaluate', {
126
+ expression: `
127
+ (function() {
128
+ const el = document.querySelector(${JSON.stringify(selector)});
129
+ if (!el) return true;
130
+ const style = window.getComputedStyle(el);
131
+ if (style.display === 'none') return true;
132
+ if (style.visibility === 'hidden') return true;
133
+ if (style.opacity === '0') return true;
134
+ const rect = el.getBoundingClientRect();
135
+ if (rect.width === 0 && rect.height === 0) return true;
136
+ return false;
137
+ })()
138
+ `,
139
+ returnByValue: true
140
+ });
141
+ return result.result.value === true;
142
+ } catch {
143
+ return true;
144
+ }
145
+ }
146
+
147
+ async function waitForHidden(selector, timeout = DEFAULT_TIMEOUT) {
148
+ const validatedTimeout = validateTimeout(timeout);
149
+ const startTime = Date.now();
150
+
151
+ while (Date.now() - startTime < validatedTimeout) {
152
+ const isHidden = await checkElementHidden(selector);
153
+ if (isHidden) return;
154
+ await sleep(POLL_INTERVAL);
155
+ }
156
+
157
+ throw timeoutError(
158
+ `Timeout (${validatedTimeout}ms) waiting for element to disappear: "${selector}"`
159
+ );
160
+ }
161
+
162
+ async function getElementCount(selector) {
163
+ try {
164
+ const result = await session.send('Runtime.evaluate', {
165
+ expression: `document.querySelectorAll(${JSON.stringify(selector)}).length`,
166
+ returnByValue: true
167
+ });
168
+ return result.result.value || 0;
169
+ } catch {
170
+ return 0;
171
+ }
172
+ }
173
+
174
+ async function waitForCount(selector, minCount, timeout = DEFAULT_TIMEOUT) {
175
+ if (typeof minCount !== 'number' || minCount < 0) {
176
+ throw new Error('minCount must be a non-negative number');
177
+ }
178
+
179
+ const validatedTimeout = validateTimeout(timeout);
180
+ const startTime = Date.now();
181
+
182
+ while (Date.now() - startTime < validatedTimeout) {
183
+ const count = await getElementCount(selector);
184
+ if (count >= minCount) return;
185
+ await sleep(POLL_INTERVAL);
186
+ }
187
+
188
+ const finalCount = await getElementCount(selector);
189
+ throw timeoutError(
190
+ `Timeout (${validatedTimeout}ms) waiting for ${minCount} elements matching "${selector}" (found ${finalCount})`
191
+ );
192
+ }
193
+
194
+ /**
195
+ * Wait for text using browser-side MutationObserver
196
+ */
197
+ async function waitForText(text, opts = {}) {
198
+ const { timeout = DEFAULT_TIMEOUT, caseSensitive = false } = opts;
199
+ const validatedTimeout = validateTimeout(timeout);
200
+
201
+ try {
202
+ // Use browser-side polling with MutationObserver
203
+ const result = await session.send('Runtime.evaluate', {
204
+ expression: `
205
+ new Promise((resolve, reject) => {
206
+ const searchText = ${JSON.stringify(text)};
207
+ const caseSensitive = ${caseSensitive};
208
+ const timeout = ${validatedTimeout};
209
+
210
+ const checkText = () => {
211
+ const bodyText = document.body ? document.body.innerText : '';
212
+ if (caseSensitive) {
213
+ return bodyText.includes(searchText);
214
+ }
215
+ return bodyText.toLowerCase().includes(searchText.toLowerCase());
216
+ };
217
+
218
+ // Check if text already exists
219
+ if (checkText()) {
220
+ resolve({ found: true, immediate: true });
221
+ return;
222
+ }
223
+
224
+ let resolved = false;
225
+ const timeoutId = setTimeout(() => {
226
+ if (!resolved) {
227
+ resolved = true;
228
+ observer.disconnect();
229
+ reject(new Error('Timeout waiting for text: ' + searchText));
230
+ }
231
+ }, timeout);
232
+
233
+ const observer = new MutationObserver((mutations, obs) => {
234
+ if (!resolved && checkText()) {
235
+ resolved = true;
236
+ obs.disconnect();
237
+ clearTimeout(timeoutId);
238
+ resolve({ found: true, mutations: mutations.length });
239
+ }
240
+ });
241
+
242
+ observer.observe(document.documentElement || document.body, {
243
+ childList: true,
244
+ subtree: true,
245
+ characterData: true
246
+ });
247
+
248
+ // Also check with RAF as a fallback
249
+ const checkWithRAF = () => {
250
+ if (resolved) return;
251
+ if (checkText()) {
252
+ resolved = true;
253
+ observer.disconnect();
254
+ clearTimeout(timeoutId);
255
+ resolve({ found: true, raf: true });
256
+ return;
257
+ }
258
+ requestAnimationFrame(checkWithRAF);
259
+ };
260
+ requestAnimationFrame(checkWithRAF);
261
+ })
262
+ `,
263
+ awaitPromise: true,
264
+ returnByValue: true
265
+ });
266
+
267
+ if (result.exceptionDetails) {
268
+ throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text);
269
+ }
270
+
271
+ return result.result.value;
272
+ } catch (error) {
273
+ // Fall back to original Node.js polling
274
+ const startTime = Date.now();
275
+ const checkExpr = caseSensitive
276
+ ? `document.body.innerText.includes(${JSON.stringify(text)})`
277
+ : `document.body.innerText.toLowerCase().includes(${JSON.stringify(text.toLowerCase())})`;
278
+
279
+ while (Date.now() - startTime < validatedTimeout) {
280
+ try {
281
+ const result = await session.send('Runtime.evaluate', {
282
+ expression: checkExpr,
283
+ returnByValue: true
284
+ });
285
+ if (result.result.value === true) return;
286
+ } catch {
287
+ // Continue polling
288
+ }
289
+ await sleep(POLL_INTERVAL);
290
+ }
291
+
292
+ throw timeoutError(
293
+ `Timeout (${validatedTimeout}ms) waiting for text: "${text}"${caseSensitive ? ' (case-sensitive)' : ''}`
294
+ );
295
+ }
296
+ }
297
+
298
+ async function waitForTextRegex(pattern, timeout = DEFAULT_TIMEOUT) {
299
+ const validatedTimeout = validateTimeout(timeout);
300
+ const startTime = Date.now();
301
+
302
+ try {
303
+ new RegExp(pattern);
304
+ } catch (e) {
305
+ throw new Error(`Invalid regex pattern: ${pattern} - ${e.message}`);
306
+ }
307
+
308
+ while (Date.now() - startTime < validatedTimeout) {
309
+ try {
310
+ const result = await session.send('Runtime.evaluate', {
311
+ expression: `
312
+ (function() {
313
+ try {
314
+ const regex = new RegExp(${JSON.stringify(pattern)});
315
+ return regex.test(document.body.innerText);
316
+ } catch {
317
+ return false;
318
+ }
319
+ })()
320
+ `,
321
+ returnByValue: true
322
+ });
323
+ if (result.result.value === true) return;
324
+ } catch {
325
+ // Continue polling
326
+ }
327
+ await sleep(POLL_INTERVAL);
328
+ }
329
+
330
+ throw timeoutError(
331
+ `Timeout (${validatedTimeout}ms) waiting for text matching pattern: /${pattern}/`
332
+ );
333
+ }
334
+
335
+ async function waitForUrlContains(substring, timeout = DEFAULT_TIMEOUT) {
336
+ const validatedTimeout = validateTimeout(timeout);
337
+ const startTime = Date.now();
338
+
339
+ while (Date.now() - startTime < validatedTimeout) {
340
+ try {
341
+ const result = await session.send('Runtime.evaluate', {
342
+ expression: 'window.location.href',
343
+ returnByValue: true
344
+ });
345
+ const currentUrl = result.result.value;
346
+ if (currentUrl && currentUrl.includes(substring)) return;
347
+ } catch {
348
+ // Continue polling
349
+ }
350
+ await sleep(POLL_INTERVAL);
351
+ }
352
+
353
+ let finalUrl = 'unknown';
354
+ try {
355
+ const result = await session.send('Runtime.evaluate', {
356
+ expression: 'window.location.href',
357
+ returnByValue: true
358
+ });
359
+ finalUrl = result.result.value || 'unknown';
360
+ } catch {
361
+ // Ignore
362
+ }
363
+
364
+ throw timeoutError(
365
+ `Timeout (${validatedTimeout}ms) waiting for URL to contain "${substring}" (current: ${finalUrl})`
366
+ );
367
+ }
368
+
369
+ async function waitForTime(ms) {
370
+ if (typeof ms !== 'number' || ms < 0) {
371
+ throw new Error('wait time must be a non-negative number');
372
+ }
373
+ await sleep(ms);
374
+ }
375
+
376
+ async function execute(params) {
377
+ if (typeof params === 'string') {
378
+ return waitForSelector(params);
379
+ }
380
+
381
+ if (params.time !== undefined) {
382
+ return waitForTime(params.time);
383
+ }
384
+
385
+ if (params.selector !== undefined) {
386
+ if (params.hidden === true) {
387
+ return waitForHidden(params.selector, params.timeout);
388
+ }
389
+ if (params.minCount !== undefined) {
390
+ return waitForCount(params.selector, params.minCount, params.timeout);
391
+ }
392
+ return waitForSelector(params.selector, params.timeout);
393
+ }
394
+
395
+ if (params.text !== undefined) {
396
+ return waitForText(params.text, {
397
+ timeout: params.timeout,
398
+ caseSensitive: params.caseSensitive
399
+ });
400
+ }
401
+
402
+ if (params.textRegex !== undefined) {
403
+ return waitForTextRegex(params.textRegex, params.timeout);
404
+ }
405
+
406
+ if (params.urlContains !== undefined) {
407
+ return waitForUrlContains(params.urlContains, params.timeout);
408
+ }
409
+
410
+ throw new Error(`Invalid wait params: ${JSON.stringify(params)}`);
411
+ }
412
+
413
+ return {
414
+ execute,
415
+ waitForSelector,
416
+ waitForHidden,
417
+ waitForCount,
418
+ waitForText,
419
+ waitForTextRegex,
420
+ waitForUrlContains,
421
+ waitForTime
422
+ };
423
+ }
package/src/index.js CHANGED
@@ -21,7 +21,7 @@ export {
21
21
  getChromeStatus,
22
22
  isChromeProcessRunning,
23
23
  createNewTab
24
- } from './cdp.js';
24
+ } from './cdp/index.js';
25
25
 
26
26
  // ============================================================================
27
27
  // Page Operations and Waiting
@@ -42,7 +42,7 @@ export {
42
42
  lcsSimilarity,
43
43
  getDOMSignature,
44
44
  waitForDOMStability
45
- } from './page.js';
45
+ } from './page/index.js';
46
46
 
47
47
  // ============================================================================
48
48
  // DOM Operations and Input
@@ -70,7 +70,7 @@ export {
70
70
  createActionabilityChecker,
71
71
  createElementValidator,
72
72
  createReactInputFiller
73
- } from './dom.js';
73
+ } from './dom/index.js';
74
74
 
75
75
  // ============================================================================
76
76
  // ARIA and Role-based Queries
@@ -97,7 +97,7 @@ export {
97
97
  createPdfCapture,
98
98
  createDebugCapture,
99
99
  createEvalSerializer
100
- } from './capture.js';
100
+ } from './capture/index.js';
101
101
 
102
102
  // ============================================================================
103
103
  // Test Execution
@@ -107,7 +107,7 @@ export {
107
107
  executeStep,
108
108
  runSteps,
109
109
  createTestRunner
110
- } from './runner.js';
110
+ } from './runner/index.js';
111
111
 
112
112
  // ============================================================================
113
113
  // Utilities and Errors
@@ -156,5 +156,5 @@ export {
156
156
  // ============================================================================
157
157
  // Default Export - High-level browser factory
158
158
  // ============================================================================
159
- import { createBrowser } from './cdp.js';
159
+ import { createBrowser } from './cdp/index.js';
160
160
  export default createBrowser;
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Cookie Manager Module
3
+ * CDP-based cookie management for getting, setting, and clearing cookies
4
+ *
5
+ * PUBLIC EXPORTS:
6
+ * - createCookieManager(session) - Factory for cookie manager
7
+ *
8
+ * @module cdp-skill/page/cookie-manager
9
+ */
10
+
11
+ /**
12
+ * Creates a cookie manager for getting, setting, and clearing cookies
13
+ * @param {import('../types.js').CDPSession} session - CDP session
14
+ * @returns {Object} Cookie manager interface
15
+ */
16
+ export function createCookieManager(session) {
17
+ /**
18
+ * Get all cookies, optionally filtered by URLs
19
+ * @param {string[]} [urls=[]] - Optional URLs to filter cookies
20
+ * @returns {Promise<import('../types.js').CookieObject[]>} Array of cookie objects
21
+ */
22
+ async function getCookies(urls = []) {
23
+ const result = await session.send('Storage.getCookies', {});
24
+ let cookies = result.cookies || [];
25
+
26
+ // Filter by URLs if provided
27
+ if (urls.length > 0) {
28
+ cookies = cookies.filter(cookie => {
29
+ return urls.some(url => {
30
+ try {
31
+ const parsed = new URL(url);
32
+ // Domain matching
33
+ const domainMatch = cookie.domain.startsWith('.')
34
+ ? parsed.hostname.endsWith(cookie.domain.slice(1))
35
+ : parsed.hostname === cookie.domain;
36
+ // Path matching
37
+ const pathMatch = parsed.pathname.startsWith(cookie.path);
38
+ // Secure matching
39
+ const secureMatch = !cookie.secure || parsed.protocol === 'https:';
40
+ return domainMatch && pathMatch && secureMatch;
41
+ } catch {
42
+ return false;
43
+ }
44
+ });
45
+ });
46
+ }
47
+
48
+ return cookies.map(cookie => ({
49
+ name: cookie.name,
50
+ value: cookie.value,
51
+ domain: cookie.domain,
52
+ path: cookie.path,
53
+ expires: cookie.expires,
54
+ httpOnly: cookie.httpOnly,
55
+ secure: cookie.secure,
56
+ sameSite: cookie.sameSite || 'Lax'
57
+ }));
58
+ }
59
+
60
+ /**
61
+ * Set one or more cookies
62
+ * @param {import('../types.js').CookieObject[]} cookies - Array of cookie objects to set
63
+ * @returns {Promise<void>}
64
+ */
65
+ async function setCookies(cookies) {
66
+ const processedCookies = cookies.map(cookie => {
67
+ const processed = {
68
+ name: cookie.name,
69
+ value: cookie.value
70
+ };
71
+
72
+ // If URL provided, derive domain/path/secure from it
73
+ if (cookie.url) {
74
+ try {
75
+ const parsed = new URL(cookie.url);
76
+ processed.domain = cookie.domain || parsed.hostname;
77
+ processed.path = cookie.path || '/';
78
+ processed.secure = cookie.secure !== undefined ? cookie.secure : parsed.protocol === 'https:';
79
+ } catch {
80
+ throw new Error(`Invalid cookie URL: ${cookie.url}`);
81
+ }
82
+ } else {
83
+ // Require domain and path if no URL
84
+ if (!cookie.domain) {
85
+ throw new Error('Cookie requires either url or domain');
86
+ }
87
+ processed.domain = cookie.domain;
88
+ processed.path = cookie.path || '/';
89
+ processed.secure = cookie.secure || false;
90
+ }
91
+
92
+ // Optional properties
93
+ if (cookie.expires !== undefined) {
94
+ processed.expires = cookie.expires;
95
+ }
96
+ if (cookie.httpOnly !== undefined) {
97
+ processed.httpOnly = cookie.httpOnly;
98
+ }
99
+ if (cookie.sameSite) {
100
+ processed.sameSite = cookie.sameSite;
101
+ }
102
+
103
+ return processed;
104
+ });
105
+
106
+ await session.send('Storage.setCookies', { cookies: processedCookies });
107
+ }
108
+
109
+ /**
110
+ * Clear all cookies or cookies matching specific domains
111
+ * @param {string[]} [urls=[]] - Optional URLs to filter cookies by domain
112
+ * @param {Object} [options] - Optional filters
113
+ * @param {string} [options.domain] - Clear cookies for specific domain
114
+ * @returns {Promise<{count: number}>} Number of cookies deleted
115
+ */
116
+ async function clearCookies(urls = [], options = {}) {
117
+ const { domain } = options;
118
+
119
+ if (urls.length === 0 && !domain) {
120
+ // Clear all cookies
121
+ const allCookies = await getCookies();
122
+ const count = allCookies.length;
123
+ await session.send('Storage.clearCookies', {});
124
+ return { count };
125
+ }
126
+
127
+ // Get cookies to filter
128
+ let cookiesToDelete;
129
+ if (domain) {
130
+ // Filter by domain - get all cookies and match domain
131
+ const allCookies = await getCookies();
132
+ cookiesToDelete = allCookies.filter(cookie =>
133
+ cookie.domain === domain ||
134
+ cookie.domain === `.${domain}` ||
135
+ cookie.domain.endsWith(`.${domain}`)
136
+ );
137
+ } else {
138
+ // Filter by URLs (original behavior)
139
+ cookiesToDelete = await getCookies(urls);
140
+ }
141
+
142
+ let deletedCount = 0;
143
+
144
+ for (const cookie of cookiesToDelete) {
145
+ try {
146
+ await session.send('Network.deleteCookies', {
147
+ name: cookie.name,
148
+ domain: cookie.domain,
149
+ path: cookie.path
150
+ });
151
+ deletedCount++;
152
+ } catch {
153
+ // Ignore individual deletion failures
154
+ }
155
+ }
156
+
157
+ return { count: deletedCount };
158
+ }
159
+
160
+ /**
161
+ * Delete specific cookies by name
162
+ * @param {string|string[]} names - Cookie name(s) to delete
163
+ * @param {Object} [options] - Optional filters
164
+ * @param {string} [options.domain] - Limit deletion to specific domain
165
+ * @param {string} [options.path] - Limit deletion to specific path
166
+ * @returns {Promise<{count: number}>} Number of cookies deleted
167
+ */
168
+ async function deleteCookies(names, options = {}) {
169
+ const nameList = Array.isArray(names) ? names : [names];
170
+ const { domain, path } = options;
171
+ let deletedCount = 0;
172
+
173
+ // Get all cookies to find matching ones
174
+ const allCookies = await getCookies();
175
+
176
+ for (const cookie of allCookies) {
177
+ if (!nameList.includes(cookie.name)) continue;
178
+ if (domain && cookie.domain !== domain && !cookie.domain.endsWith(`.${domain}`)) continue;
179
+ if (path && cookie.path !== path) continue;
180
+
181
+ try {
182
+ await session.send('Network.deleteCookies', {
183
+ name: cookie.name,
184
+ domain: cookie.domain,
185
+ path: cookie.path
186
+ });
187
+ deletedCount++;
188
+ } catch {
189
+ // Ignore individual deletion failures
190
+ }
191
+ }
192
+
193
+ return { count: deletedCount };
194
+ }
195
+
196
+ return {
197
+ getCookies,
198
+ setCookies,
199
+ clearCookies,
200
+ deleteCookies
201
+ };
202
+ }