claude-chrome-parallel 3.4.0 → 3.4.2

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 (55) hide show
  1. package/README.md +114 -48
  2. package/dist/cdp/client.d.ts +17 -4
  3. package/dist/cdp/client.d.ts.map +1 -1
  4. package/dist/cdp/client.js +144 -100
  5. package/dist/cdp/client.js.map +1 -1
  6. package/dist/cdp/connection-pool.d.ts +5 -1
  7. package/dist/cdp/connection-pool.d.ts.map +1 -1
  8. package/dist/cdp/connection-pool.js +40 -38
  9. package/dist/cdp/connection-pool.js.map +1 -1
  10. package/dist/chrome/launcher.js +2 -2
  11. package/dist/chrome/launcher.js.map +1 -1
  12. package/dist/dashboard/activity-tracker.d.ts +16 -0
  13. package/dist/dashboard/activity-tracker.d.ts.map +1 -1
  14. package/dist/dashboard/activity-tracker.js +49 -8
  15. package/dist/dashboard/activity-tracker.js.map +1 -1
  16. package/dist/hints/hint-engine.d.ts +13 -1
  17. package/dist/hints/hint-engine.d.ts.map +1 -1
  18. package/dist/hints/hint-engine.js +41 -6
  19. package/dist/hints/hint-engine.js.map +1 -1
  20. package/dist/hints/rules/error-recovery.d.ts.map +1 -1
  21. package/dist/hints/rules/error-recovery.js +4 -0
  22. package/dist/hints/rules/error-recovery.js.map +1 -1
  23. package/dist/hints/rules/sequence-detection.d.ts.map +1 -1
  24. package/dist/hints/rules/sequence-detection.js +28 -0
  25. package/dist/hints/rules/sequence-detection.js.map +1 -1
  26. package/dist/mcp-server.d.ts.map +1 -1
  27. package/dist/mcp-server.js +18 -4
  28. package/dist/mcp-server.js.map +1 -1
  29. package/dist/orchestration/state-manager.d.ts +12 -2
  30. package/dist/orchestration/state-manager.d.ts.map +1 -1
  31. package/dist/orchestration/state-manager.js +24 -21
  32. package/dist/orchestration/state-manager.js.map +1 -1
  33. package/dist/orchestration/workflow-engine.d.ts +34 -4
  34. package/dist/orchestration/workflow-engine.d.ts.map +1 -1
  35. package/dist/orchestration/workflow-engine.js +235 -54
  36. package/dist/orchestration/workflow-engine.js.map +1 -1
  37. package/dist/session-manager.d.ts.map +1 -1
  38. package/dist/session-manager.js +67 -7
  39. package/dist/session-manager.js.map +1 -1
  40. package/dist/tools/click-element.d.ts.map +1 -1
  41. package/dist/tools/click-element.js +112 -40
  42. package/dist/tools/click-element.js.map +1 -1
  43. package/dist/tools/computer.d.ts.map +1 -1
  44. package/dist/tools/computer.js +7 -7
  45. package/dist/tools/computer.js.map +1 -1
  46. package/dist/tools/javascript.d.ts.map +1 -1
  47. package/dist/tools/javascript.js +6 -2
  48. package/dist/tools/javascript.js.map +1 -1
  49. package/dist/tools/orchestration.d.ts.map +1 -1
  50. package/dist/tools/orchestration.js +51 -0
  51. package/dist/tools/orchestration.js.map +1 -1
  52. package/dist/tools/read-page.d.ts.map +1 -1
  53. package/dist/tools/read-page.js +52 -8
  54. package/dist/tools/read-page.js.map +1 -1
  55. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Claude Chrome Parallel
1
+ # Claude Chrome Parallel (CCP)
2
2
 
3
3
  > **Ultrafast parallel browser MCP.**
4
4
 
@@ -37,7 +37,16 @@ CCP (parallel, zero auth):
37
37
 
38
38
  ## Why CCP Is Fast
39
39
 
40
- The speed advantage **compounds** with every site. Playwright MCP has two bottlenecks that multiply:
40
+ This is not a speed optimization. It's a **structural change**.
41
+
42
+ ```
43
+ Playwright MCP: [blank browser] → login → task → close (repeat per site)
44
+ CCP: [your Chrome] → task (already logged in)
45
+ ```
46
+
47
+ Playwright creates a new browser per site. Each one needs: navigate → type email → type password → solve 2FA → wait for redirect. That's 30-120s per site, and it's sequential. **You're spending 95% of the time on authentication, not the actual task.**
48
+
49
+ CCP connects to your existing Chrome via CDP. You're already logged in to everything. Workers run in parallel. The speed advantage **compounds** with every site:
41
50
 
42
51
  | Sites | Playwright MCP | CCP | Speedup |
43
52
  |:-----:|:--------------:|:---:|:-------:|
@@ -46,16 +55,18 @@ The speed advantage **compounds** with every site. Playwright MCP has two bottle
46
55
  | 5 | ~250s | ~3s | **80x** |
47
56
  | 10 | ~500s | ~3s | **160x** |
48
57
 
49
- **Why?** Playwright launches a blank browser per site. Each needs: navigate → type email → type password → solve 2FA → wait for redirect. That's 30-120s **per site**, and it's **sequential**.
50
-
51
- CCP connects to your existing Chrome. You're already logged into everything. Workers run in parallel. The task that takes 250s with Playwright takes 3s with CCP.
52
-
53
58
  ### Memory
54
59
 
55
60
  Playwright spawns a **separate browser process** per session (~500MB each). Five sites = 2.5GB.
56
61
 
57
62
  CCP uses **one Chrome** with lightweight browser contexts (like incognito windows sharing the same process). Five Workers = ~300MB total. That's **8x less memory** — and it stays flat whether you run 5 or 20 Workers.
58
63
 
64
+ ### Bot Detection Immunity
65
+
66
+ Playwright runs headless browsers with detectable fingerprints. Cloudflare, reCAPTCHA, and anti-bot systems can flag them.
67
+
68
+ CCP uses **your actual Chrome** — real fingerprint, real cookies, real browsing history. It's indistinguishable from you clicking around manually, because it literally is your browser.
69
+
59
70
  ---
60
71
 
61
72
  ## Core Features
@@ -168,54 +179,84 @@ Claude: [3 Workers, 3 sites, simultaneously]
168
179
 
169
180
  ## What You Can Do
170
181
 
171
- ### Multi-Account Operations
182
+ ### 20-Site Parallel Crawling
172
183
 
173
184
  ```
174
- You: ccp check my personal and work Gmail at the same time
175
-
176
- Claude: [Creates 2 isolated Workers]
177
- Personal account: 3 unread emails
178
- Work account: 7 unread emails
179
- Time: 2.1s (parallel)
185
+ You: ccp crawl these 20 competitor sites and extract their pricing
186
+
187
+ Claude: [20 Workers, 20 sites, simultaneously — all in your logged-in Chrome]
188
+ site-01: $49/mo ✓ (1.2s)
189
+ site-02: $59/mo ✓ (0.9s)
190
+ ...
191
+ site-20: $39/mo ✓ (1.4s)
192
+ Total: 2.8s | Sequential: ~60s | Speedup: 21x
180
193
  ```
181
194
 
182
- ### Price Comparison Across Sites
195
+ Each Worker runs in an isolated browser context. One Chrome process, 20 parallel sessions, ~300MB total. Not 20 separate browsers eating 10GB of RAM.
183
196
 
184
- ```
185
- You: ccp find the cheapest iPhone 15 on Amazon, eBay, and Walmart
197
+ ### Multi-Cloud Dashboard Monitoring
186
198
 
187
- Claude: [3 Workers, 3 sites, simultaneously]
188
- Amazon: $999
189
- eBay: $945 ← lowest
190
- Walmart: $979
191
- Time: 1.3s total (not 3.9s)
192
199
  ```
200
+ You: ccp screenshot my AWS billing, GCP console, Azure portal, Stripe,
201
+ and Datadog — all at once
202
+
203
+ Claude: [5 Workers — already logged into every cloud provider]
204
+ aws-billing.png $12,847/mo ✓
205
+ gcp-console.png $8,291/mo ✓
206
+ azure-portal.png $3,104/mo ✓
207
+ stripe-revenue.png $47,230 MRR ✓
208
+ datadog-metrics.png 99.7% uptime ✓
209
+ Time: 3.1s (not 10+ minutes of login screens)
210
+ ```
211
+
212
+ No OAuth tokens. No service accounts. No API keys to rotate. You're already logged in.
193
213
 
194
- ### Authenticated Dashboard Monitoring
214
+ ### Automated Regression Testing
195
215
 
216
+ ```bash
217
+ # Full regression suite — 10 flows, 10 Workers, one command
218
+ claude -p "ccp test these 10 critical flows on staging.myapp.com:
219
+ login, signup, checkout, search, profile-edit,
220
+ password-reset, file-upload, notifications, admin-panel, API-docs"
221
+
222
+ # Sequential: ~15 minutes. CCP: ~90 seconds.
223
+ # Run this before every deploy.
196
224
  ```
197
- You: ccp screenshot my AWS billing, Stripe dashboard, and Vercel usage
198
225
 
199
- Claude: [All 3 require loginbut you're already authenticated]
200
- aws-billing.png ✓
201
- stripe-revenue.png
202
- vercel-usage.png ✓
226
+ Each Worker gets an isolated session no cookie contamination between test flows. Test multi-user scenarios (admin + regular user) simultaneously.
227
+
228
+ ### Competitive Intelligence at Scale
229
+
230
+ ```
231
+ You: ccp monitor pricing on Amazon, Walmart, Target, Best Buy, and Costco
232
+ for "Sony WH-1000XM5" — compare and find the lowest
233
+
234
+ Claude: [5 Workers, 5 retailers, parallel]
235
+ Amazon: $278
236
+ Walmart: $298
237
+ Target: $279
238
+ Best Buy: $249 ← lowest (sale)
239
+ Costco: $269 (members only)
240
+ Time: 2.4s | All prices from live pages, not cached APIs
203
241
  ```
204
242
 
205
- ### Parallel QA Testing
243
+ Works on sites with bot detection because it's your real Chrome — real cookies, real fingerprint, real browsing history.
206
244
 
207
- ```bash
208
- # 5 tests, 5 terminals, all at once
209
- claude -p "ccp test login flow on myapp.com" # Worker 1
210
- claude -p "ccp test checkout on myapp.com" # Worker 2
211
- claude -p "ccp test admin panel on myapp.com" # Worker 3
212
- claude -p "ccp test mobile view on myapp.com" # Worker 4
213
- claude -p "ccp test form validation on myapp.com" # Worker 5
214
-
215
- # Sequential: ~5 minutes. Parallel with CCP: ~1 minute.
216
- # That's your entire smoke test suite before lunch.
245
+ ### Multi-Account Operations
246
+
247
+ ```
248
+ You: ccp check order status on my personal and business Amazon accounts,
249
+ plus my eBay seller dashboard all at the same time
250
+
251
+ Claude: [3 Workers, 3 isolated sessions]
252
+ Amazon Personal: 2 packages arriving tomorrow
253
+ Amazon Business: Purchase order #4521 approved
254
+ eBay Seller: 3 new orders, $847 revenue today
255
+ Time: 2.1s
217
256
  ```
218
257
 
258
+ Same site, different accounts, simultaneously. Each Worker has its own cookies and session state.
259
+
219
260
  ---
220
261
 
221
262
  ## Comparison
@@ -240,7 +281,9 @@ claude -p "ccp test form validation on myapp.com" # Worker 5
240
281
 
241
282
  ## Adaptive Guidance
242
283
 
243
- Every tool response includes contextual hints that prevent the LLM from wasting time on wrong approaches. 21 static rules + an adaptive memory system that learns from your usage patterns.
284
+ The biggest time sink in LLM browser automation isn't execution speed it's **wrong tool choices, missed page state, and pointless retries**. Each mistake costs 3-10 seconds of LLM inference. Three mistakes and you've wasted 30 seconds before anything useful happens.
285
+
286
+ CCP injects contextual `_hint` fields into every tool response to prevent this:
244
287
 
245
288
  ```
246
289
  click_element → Error: "ref not found"
@@ -250,9 +293,13 @@ click_element → Error: "ref not found"
250
293
  navigate → title contains "Login"
251
294
  _hint: "Login page detected. Use fill_form for credentials."
252
295
  → LLM skips straight to form filling.
296
+
297
+ find → computer(click) pattern detected
298
+ _hint: "Use click_element to find+click in one call."
299
+ → Eliminates unnecessary intermediate steps.
253
300
  ```
254
301
 
255
- **Adaptive Memory**: The system observes which tool resolves each error type. After seeing the same recovery pattern 3 times, it promotes it to a permanent hint — persisted across sessions in `.chrome-parallel/hints/learned-patterns.json`.
302
+ 21 static rules across 6 priority tiers + an **adaptive memory** system that learns from your usage. When the same error→recovery pattern appears 3 times, it's promoted to a permanent hint — persisted across sessions in `.chrome-parallel/hints/learned-patterns.json`.
256
303
 
257
304
  <details>
258
305
  <summary>Rule priority tiers</summary>
@@ -356,16 +403,18 @@ navigate → title contains "Login"
356
403
  ## CLI
357
404
 
358
405
  ```bash
359
- npx claude-chrome-parallel setup # Auto-configure (global)
360
- npx claude-chrome-parallel setup --scope project # Auto-configure (project only)
361
- npx claude-chrome-parallel serve --auto-launch # Start with auto Chrome launch
362
- npx claude-chrome-parallel serve --headless-shell # Headless mode (15-30% less memory)
363
- npx claude-chrome-parallel serve -p <port> # Custom debugging port (default: 9222)
364
- npx claude-chrome-parallel doctor # Diagnose installation
365
- npx claude-chrome-parallel status # View sessions
366
- npx claude-chrome-parallel cleanup # Clean up old sessions
406
+ ccp setup # Auto-configure (global)
407
+ ccp setup --scope project # Auto-configure (project only)
408
+ ccp serve --auto-launch # Start with auto Chrome launch
409
+ ccp serve --headless-shell # Headless mode (15-30% less memory)
410
+ ccp serve -p <port> # Custom debugging port (default: 9222)
411
+ ccp doctor # Diagnose installation
412
+ ccp status # View sessions
413
+ ccp cleanup # Clean up old sessions
367
414
  ```
368
415
 
416
+ > `ccp` requires global install (`npm i -g claude-chrome-parallel`). All commands also work via `npx claude-chrome-parallel <command>`.
417
+
369
418
  ---
370
419
 
371
420
  <details>
@@ -389,6 +438,23 @@ cd claude-chrome-parallel
389
438
  npm install && npm run build && npm test
390
439
  ```
391
440
 
441
+ ## Compatibility
442
+
443
+ CCP is a standard **MCP server** (stdio JSON-RPC). While optimized for Claude Code, it works with any MCP-compatible client:
444
+
445
+ ```json
446
+ {
447
+ "mcpServers": {
448
+ "chrome-parallel": {
449
+ "command": "npx",
450
+ "args": ["-y", "claude-chrome-parallel", "serve", "--auto-launch"]
451
+ }
452
+ }
453
+ }
454
+ ```
455
+
456
+ Cursor, Windsurf, Codex CLI, or any editor that supports MCP can use CCP with the config above. See [Contributing](CONTRIBUTING.md) for multi-client testing status.
457
+
392
458
  ## License
393
459
 
394
460
  MIT — [LICENSE](LICENSE)
@@ -32,6 +32,8 @@ export declare class CDPClient {
32
32
  private autoLaunch;
33
33
  private cookieSourceCache;
34
34
  private cookieDataCache;
35
+ private targetIdIndex;
36
+ private inFlightCookieScans;
35
37
  private static readonly COOKIE_CACHE_TTL;
36
38
  constructor(options?: CDPClientOptions);
37
39
  /**
@@ -117,14 +119,25 @@ export declare class CDPClient {
117
119
  */
118
120
  private domainMatchScore;
119
121
  /**
120
- * Find an authenticated page with cookies to copy from
121
- * Returns the targetId of a page that has cookies in Chrome's default context
122
+ * Find an authenticated page with cookies to copy from.
123
+ * Returns the targetId of a page that has cookies in Chrome's default context.
124
+ *
125
+ * Promise coalescing: concurrent callers for the same domain share one probe
126
+ * instead of independently hammering Chrome with 20 simultaneous scans.
127
+ *
122
128
  * @param targetDomain Optional domain to prioritize when selecting cookie source
123
129
  */
124
130
  findAuthenticatedPageTargetId(targetDomain?: string): Promise<string | null>;
125
131
  /**
126
- * Copy all cookies from authenticated page to destination page
127
- * Uses raw WebSocket CDP connection to bypass Puppeteer's context isolation
132
+ * Internal implementation of the authenticated-page probe.
133
+ * Uses Target.attachToTarget (multiplexed CDP) instead of raw WebSocket connections.
134
+ * Uses Target.getTargets result directly instead of /json/list HTTP calls.
135
+ */
136
+ private _doFindAuthenticatedPageTargetId;
137
+ /**
138
+ * Copy all cookies from authenticated page to destination page.
139
+ * Uses Target.attachToTarget (multiplexed CDP) to bypass Puppeteer's context isolation —
140
+ * no raw WebSocket connections, no /json/list HTTP calls.
128
141
  */
129
142
  copyCookiesViaCDP(sourceTargetId: string, destPage: Page): Promise<number>;
130
143
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/cdp/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/cdp/client.ts"],"names":[],"mappings":"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"}
@@ -32,7 +32,9 @@ class CDPClient {
32
32
  autoLaunch;
33
33
  cookieSourceCache = new Map();
34
34
  cookieDataCache = new Map();
35
- static COOKIE_CACHE_TTL = 30000; // 30 seconds
35
+ targetIdIndex = new Map();
36
+ inFlightCookieScans = new Map();
37
+ static COOKIE_CACHE_TTL = 300000; // 5 minutes
36
38
  constructor(options = {}) {
37
39
  const globalConfig = (0, global_1.getGlobalConfig)();
38
40
  this.port = options.port || globalConfig.port;
@@ -82,6 +84,7 @@ class CDPClient {
82
84
  }
83
85
  // Clean up cookie data cache for this target
84
86
  this.cookieDataCache.delete(targetId);
87
+ this.targetIdIndex.delete(targetId);
85
88
  for (const listener of this.targetDestroyedListeners) {
86
89
  try {
87
90
  listener(targetId);
@@ -158,6 +161,8 @@ class CDPClient {
158
161
  });
159
162
  // Clear existing sessions
160
163
  this.sessions.clear();
164
+ this.targetIdIndex.clear();
165
+ this.inFlightCookieScans.clear();
161
166
  this.browser = null;
162
167
  // Attempt reconnection
163
168
  while (this.reconnectAttempts < this.maxReconnectAttempts) {
@@ -212,6 +217,20 @@ class CDPClient {
212
217
  console.error(`[CDPClient] Target destroyed: ${targetId}`);
213
218
  this.onTargetDestroyed(targetId);
214
219
  });
220
+ // Maintain target-to-page index for O(1) lookups
221
+ this.browser.on('targetcreated', async (target) => {
222
+ if (target.type() === 'page') {
223
+ try {
224
+ const page = await target.page();
225
+ if (page) {
226
+ this.targetIdIndex.set(getTargetId(target), page);
227
+ }
228
+ }
229
+ catch {
230
+ // Target may have been destroyed before we could index it
231
+ }
232
+ }
233
+ });
215
234
  this.connectionState = 'connected';
216
235
  this.emitConnectionEvent({
217
236
  type: 'connected',
@@ -371,8 +390,12 @@ class CDPClient {
371
390
  }
372
391
  }
373
392
  /**
374
- * Find an authenticated page with cookies to copy from
375
- * Returns the targetId of a page that has cookies in Chrome's default context
393
+ * Find an authenticated page with cookies to copy from.
394
+ * Returns the targetId of a page that has cookies in Chrome's default context.
395
+ *
396
+ * Promise coalescing: concurrent callers for the same domain share one probe
397
+ * instead of independently hammering Chrome with 20 simultaneous scans.
398
+ *
376
399
  * @param targetDomain Optional domain to prioritize when selecting cookie source
377
400
  */
378
401
  async findAuthenticatedPageTargetId(targetDomain) {
@@ -383,7 +406,28 @@ class CDPClient {
383
406
  console.error(`[CDPClient] Cache hit for cookie source (domain: ${cacheKey}): ${cached.targetId.slice(0, 8)}`);
384
407
  return cached.targetId;
385
408
  }
386
- const WebSocket = require('ws');
409
+ // Promise coalescing: if a scan for this domain is already in-flight, reuse it
410
+ const existing = this.inFlightCookieScans.get(cacheKey);
411
+ if (existing) {
412
+ console.error(`[CDPClient] Coalescing cookie scan for domain: ${cacheKey}`);
413
+ return existing;
414
+ }
415
+ // Start the scan and register it so concurrent callers share this promise
416
+ const scanPromise = this._doFindAuthenticatedPageTargetId(targetDomain, cacheKey);
417
+ this.inFlightCookieScans.set(cacheKey, scanPromise);
418
+ try {
419
+ return await scanPromise;
420
+ }
421
+ finally {
422
+ this.inFlightCookieScans.delete(cacheKey);
423
+ }
424
+ }
425
+ /**
426
+ * Internal implementation of the authenticated-page probe.
427
+ * Uses Target.attachToTarget (multiplexed CDP) instead of raw WebSocket connections.
428
+ * Uses Target.getTargets result directly instead of /json/list HTTP calls.
429
+ */
430
+ async _doFindAuthenticatedPageTargetId(targetDomain, cacheKey) {
387
431
  const browser = this.getBrowser();
388
432
  const session = await browser.target().createCDPSession();
389
433
  try {
@@ -417,42 +461,34 @@ class CDPClient {
417
461
  });
418
462
  console.error(`[CDPClient] Sorted ${candidates.length} candidates by domain match to ${targetDomain}`);
419
463
  }
420
- // Get target list with WebSocket URLs
421
- const listResponse = await fetch(`http://127.0.0.1:${this.port}/json/list`);
422
- const targets = await listResponse.json();
423
- // Check each candidate to find one with actual cookies (in priority order)
464
+ // Check each candidate to find one with actual cookies (in priority order).
465
+ // Uses Target.attachToTarget over the existing multiplexed session — no raw WebSocket,
466
+ // no /json/list HTTP round-trip.
424
467
  for (const candidate of candidates) {
425
- const targetInfo = targets.find(t => t.id === candidate.targetId);
426
- if (!targetInfo || !targetInfo.webSocketDebuggerUrl) {
427
- continue;
428
- }
429
- // Check if this page has cookies (pages in Chrome's default context have cookies,
430
- // while Puppeteer-created pages have empty cookies)
431
- const cookieCount = await new Promise((resolve) => {
432
- const ws = new WebSocket(targetInfo.webSocketDebuggerUrl);
433
- const timeout = setTimeout(() => { ws.close(); resolve(0); }, 2000);
434
- ws.on('open', () => {
435
- ws.send(JSON.stringify({ id: 1, method: 'Network.getAllCookies' }));
436
- });
437
- ws.on('message', (data) => {
438
- const msg = JSON.parse(data.toString());
439
- if (msg.id === 1) {
440
- clearTimeout(timeout);
441
- ws.close();
442
- resolve(msg.result?.cookies?.length || 0);
443
- }
444
- });
445
- ws.on('error', () => {
446
- clearTimeout(timeout);
447
- resolve(0);
468
+ let attachedSessionId = null;
469
+ try {
470
+ const { sessionId } = await session.send('Target.attachToTarget', {
471
+ targetId: candidate.targetId,
472
+ flatten: true,
448
473
  });
449
- });
450
- if (cookieCount > 0) {
451
- const domainScore = targetDomain ? this.domainMatchScore(candidate.url, targetDomain) : 0;
452
- console.error(`[CDPClient] Found authenticated page ${candidate.targetId.slice(0, 8)} at ${candidate.url.slice(0, 50)} (${cookieCount} cookies, domain score: ${domainScore})`);
453
- // Store in cache
454
- this.cookieSourceCache.set(cacheKey, { targetId: candidate.targetId, timestamp: Date.now() });
455
- return candidate.targetId;
474
+ attachedSessionId = sessionId;
475
+ // Send Network.getAllCookies through the flat CDP session
476
+ const result = await session.send('Network.getAllCookies', undefined, { sessionId });
477
+ const cookieCount = result?.cookies?.length || 0;
478
+ if (cookieCount > 0) {
479
+ const domainScore = targetDomain ? this.domainMatchScore(candidate.url, targetDomain) : 0;
480
+ console.error(`[CDPClient] Found authenticated page ${candidate.targetId.slice(0, 8)} at ${candidate.url.slice(0, 50)} (${cookieCount} cookies, domain score: ${domainScore})`);
481
+ this.cookieSourceCache.set(cacheKey, { targetId: candidate.targetId, timestamp: Date.now() });
482
+ return candidate.targetId;
483
+ }
484
+ }
485
+ catch {
486
+ // Target may be unresponsive or already detached — skip
487
+ }
488
+ finally {
489
+ if (attachedSessionId) {
490
+ await session.send('Target.detachFromTarget', { sessionId: attachedSessionId }).catch(() => { });
491
+ }
456
492
  }
457
493
  }
458
494
  console.error('[CDPClient] No pages with cookies found');
@@ -463,17 +499,17 @@ class CDPClient {
463
499
  }
464
500
  }
465
501
  /**
466
- * Copy all cookies from authenticated page to destination page
467
- * Uses raw WebSocket CDP connection to bypass Puppeteer's context isolation
502
+ * Copy all cookies from authenticated page to destination page.
503
+ * Uses Target.attachToTarget (multiplexed CDP) to bypass Puppeteer's context isolation
504
+ * no raw WebSocket connections, no /json/list HTTP calls.
468
505
  */
469
506
  async copyCookiesViaCDP(sourceTargetId, destPage) {
470
- const WebSocket = require('ws');
471
507
  console.error(`[CDPClient] copyCookiesViaCDP called with sourceTargetId: ${sourceTargetId.slice(0, 8)}`);
472
508
  try {
473
- // Check cookie data cache first
509
+ // Check cookie data cache first — avoids re-probing Chrome entirely
474
510
  const cachedData = this.cookieDataCache.get(sourceTargetId);
475
511
  if (cachedData && Date.now() - cachedData.timestamp < CDPClient.COOKIE_CACHE_TTL) {
476
- console.error(`[CDPClient] Cache hit for cookie data (${cachedData.cookies.length} cookies), skipping WebSocket`);
512
+ console.error(`[CDPClient] Cache hit for cookie data (${cachedData.cookies.length} cookies), skipping CDP attach`);
477
513
  const destSession = await destPage.createCDPSession();
478
514
  try {
479
515
  const cookiesToSet = cachedData.cookies.map(c => ({
@@ -494,69 +530,65 @@ class CDPClient {
494
530
  await destSession.detach().catch(() => { });
495
531
  }
496
532
  }
497
- // Get target's WebSocket URL
498
- const listResponse = await fetch(`http://127.0.0.1:${this.port}/json/list`);
499
- const targets = await listResponse.json();
500
- const sourceTarget = targets.find(t => t.id === sourceTargetId);
501
- if (!sourceTarget || !sourceTarget.webSocketDebuggerUrl) {
502
- console.error(`[CDPClient] Source target not found in /json/list. Looking for: ${sourceTargetId}`);
503
- console.error(`[CDPClient] Available targets: ${targets.map(t => t.id.slice(0, 8) + ' ' + t.url.slice(0, 40)).join(', ')}`);
504
- return 0;
505
- }
506
- console.error(`[CDPClient] Connecting to source target at ${sourceTarget.url.slice(0, 50)}`);
507
- // Get cookies via WebSocket
508
- const cookies = await new Promise((resolve, reject) => {
509
- const ws = new WebSocket(sourceTarget.webSocketDebuggerUrl);
510
- const timeout = setTimeout(() => {
511
- ws.close();
512
- reject(new Error('WebSocket timeout'));
513
- }, 5000);
514
- ws.on('open', () => {
515
- ws.send(JSON.stringify({ id: 1, method: 'Network.getAllCookies' }));
516
- });
517
- ws.on('message', (data) => {
518
- const msg = JSON.parse(data.toString());
519
- if (msg.id === 1) {
520
- clearTimeout(timeout);
521
- ws.close();
522
- resolve(msg.result?.cookies || []);
523
- }
524
- });
525
- ws.on('error', (err) => {
526
- clearTimeout(timeout);
527
- reject(err);
528
- });
529
- });
530
- // Store in cookie data cache
531
- this.cookieDataCache.set(sourceTargetId, { cookies, timestamp: Date.now() });
532
- if (cookies.length === 0) {
533
- console.error(`[CDPClient] No cookies found in source page`);
534
- return 0;
535
- }
536
- console.error(`[CDPClient] Found ${cookies.length} cookies, setting on destination page`);
537
- // Set cookies on destination page
538
- const destSession = await destPage.createCDPSession();
533
+ // Attach to the source target via the multiplexed browser CDP session
534
+ const browser = this.getBrowser();
535
+ const browserSession = await browser.target().createCDPSession();
536
+ let attachedSessionId = null;
539
537
  try {
540
- const cookiesToSet = cookies.map(c => ({
541
- name: c.name,
542
- value: c.value,
543
- domain: c.domain,
544
- path: c.path,
545
- expires: c.expires,
546
- httpOnly: c.httpOnly,
547
- secure: c.secure,
548
- sameSite: c.sameSite,
549
- }));
550
- await destSession.send('Network.setCookies', { cookies: cookiesToSet });
551
- console.error(`[CDPClient] Successfully copied ${cookies.length} cookies`);
552
- return cookies.length;
538
+ // Verify the target exists before attaching
539
+ const { targetInfos } = await browserSession.send('Target.getTargets');
540
+ const sourceInfo = targetInfos.find(t => t.targetId === sourceTargetId);
541
+ if (!sourceInfo) {
542
+ console.error(`[CDPClient] Source target not found: ${sourceTargetId.slice(0, 8)}`);
543
+ console.error(`[CDPClient] Available targets: ${targetInfos.map(t => t.targetId.slice(0, 8) + ' ' + t.url.slice(0, 40)).join(', ')}`);
544
+ return 0;
545
+ }
546
+ console.error(`[CDPClient] Attaching to source target at ${sourceInfo.url.slice(0, 50)}`);
547
+ const { sessionId } = await browserSession.send('Target.attachToTarget', {
548
+ targetId: sourceTargetId,
549
+ flatten: true,
550
+ });
551
+ attachedSessionId = sessionId;
552
+ // Fetch cookies through the flat session (no raw WebSocket, no /json/list)
553
+ const result = await browserSession.send('Network.getAllCookies', undefined, { sessionId });
554
+ const cookies = result?.cookies || [];
555
+ // Store in cookie data cache
556
+ this.cookieDataCache.set(sourceTargetId, { cookies, timestamp: Date.now() });
557
+ if (cookies.length === 0) {
558
+ console.error('[CDPClient] No cookies found in source page');
559
+ return 0;
560
+ }
561
+ console.error(`[CDPClient] Found ${cookies.length} cookies, setting on destination page`);
562
+ // Set cookies on destination page via its own CDPSession
563
+ const destSession = await destPage.createCDPSession();
564
+ try {
565
+ const cookiesToSet = cookies.map(c => ({
566
+ name: c.name,
567
+ value: c.value,
568
+ domain: c.domain,
569
+ path: c.path,
570
+ expires: c.expires,
571
+ httpOnly: c.httpOnly,
572
+ secure: c.secure,
573
+ sameSite: c.sameSite,
574
+ }));
575
+ await destSession.send('Network.setCookies', { cookies: cookiesToSet });
576
+ console.error(`[CDPClient] Successfully copied ${cookies.length} cookies`);
577
+ return cookies.length;
578
+ }
579
+ finally {
580
+ await destSession.detach().catch(() => { });
581
+ }
553
582
  }
554
583
  finally {
555
- await destSession.detach().catch(() => { });
584
+ if (attachedSessionId) {
585
+ await browserSession.send('Target.detachFromTarget', { sessionId: attachedSessionId }).catch(() => { });
586
+ }
587
+ await browserSession.detach().catch(() => { });
556
588
  }
557
589
  }
558
590
  catch (error) {
559
- console.error(`[CDPClient] Error in copyCookiesViaCDP:`, error);
591
+ console.error('[CDPClient] Error in copyCookiesViaCDP:', error);
560
592
  return 0;
561
593
  }
562
594
  }
@@ -611,14 +643,26 @@ class CDPClient {
611
643
  * Get page by target ID
612
644
  */
613
645
  async getPageByTargetId(targetId) {
646
+ // Fast path: check index first (O(1))
647
+ const indexed = this.targetIdIndex.get(targetId);
648
+ if (indexed && !indexed.isClosed()) {
649
+ return indexed;
650
+ }
651
+ // Fallback: linear scan (for pages created before indexing started)
614
652
  const browser = this.getBrowser();
615
653
  const targets = browser.targets();
616
654
  for (const target of targets) {
617
655
  if (getTargetId(target) === targetId && target.type() === 'page') {
618
656
  const page = await target.page();
657
+ if (page) {
658
+ // Populate index for future lookups
659
+ this.targetIdIndex.set(targetId, page);
660
+ }
619
661
  return page;
620
662
  }
621
663
  }
664
+ // Clean stale index entry
665
+ this.targetIdIndex.delete(targetId);
622
666
  return null;
623
667
  }
624
668
  /**