@tangle-network/browser-agent-driver 0.8.7 → 0.11.0

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 (102) hide show
  1. package/README.md +2 -2
  2. package/dist/brain/index.d.ts +2 -0
  3. package/dist/brain/index.d.ts.map +1 -1
  4. package/dist/brain/index.js +96 -10
  5. package/dist/brain/index.js.map +1 -1
  6. package/dist/browser-launch.d.ts +3 -0
  7. package/dist/browser-launch.d.ts.map +1 -1
  8. package/dist/browser-launch.js +12 -2
  9. package/dist/browser-launch.js.map +1 -1
  10. package/dist/cli-design-audit.d.ts.map +1 -1
  11. package/dist/cli-design-audit.js +250 -2
  12. package/dist/cli-design-audit.js.map +1 -1
  13. package/dist/cli-showcase.d.ts +28 -0
  14. package/dist/cli-showcase.d.ts.map +1 -0
  15. package/dist/cli-showcase.js +131 -0
  16. package/dist/cli-showcase.js.map +1 -0
  17. package/dist/cli-ui.d.ts.map +1 -1
  18. package/dist/cli-ui.js +35 -2
  19. package/dist/cli-ui.js.map +1 -1
  20. package/dist/cli.js +462 -217
  21. package/dist/cli.js.map +1 -1
  22. package/dist/config.d.ts +5 -1
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +3 -2
  25. package/dist/config.js.map +1 -1
  26. package/dist/design/compare.d.ts +9 -0
  27. package/dist/design/compare.d.ts.map +1 -0
  28. package/dist/design/compare.js +339 -0
  29. package/dist/design/compare.js.map +1 -0
  30. package/dist/design/index.d.ts +6 -0
  31. package/dist/design/index.d.ts.map +1 -0
  32. package/dist/design/index.js +5 -0
  33. package/dist/design/index.js.map +1 -0
  34. package/dist/design/page-interaction.d.ts +24 -0
  35. package/dist/design/page-interaction.d.ts.map +1 -0
  36. package/dist/design/page-interaction.js +244 -0
  37. package/dist/design/page-interaction.js.map +1 -0
  38. package/dist/design/rip.d.ts +9 -0
  39. package/dist/design/rip.d.ts.map +1 -0
  40. package/dist/design/rip.js +362 -0
  41. package/dist/design/rip.js.map +1 -0
  42. package/dist/design/types.d.ts +126 -0
  43. package/dist/design/types.d.ts.map +1 -0
  44. package/dist/design/types.js +3 -0
  45. package/dist/design/types.js.map +1 -0
  46. package/dist/design-audit.js +2 -2
  47. package/dist/design-audit.js.map +1 -1
  48. package/dist/index.d.ts +10 -4
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +6 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/memory/knowledge.d.ts +25 -9
  53. package/dist/memory/knowledge.d.ts.map +1 -1
  54. package/dist/memory/knowledge.js +94 -28
  55. package/dist/memory/knowledge.js.map +1 -1
  56. package/dist/memory/run-registry.d.ts +85 -0
  57. package/dist/memory/run-registry.d.ts.map +1 -0
  58. package/dist/memory/run-registry.js +144 -0
  59. package/dist/memory/run-registry.js.map +1 -0
  60. package/dist/multi-actor.d.ts +3 -3
  61. package/dist/multi-actor.d.ts.map +1 -1
  62. package/dist/multi-actor.js +3 -3
  63. package/dist/multi-actor.js.map +1 -1
  64. package/dist/runner/index.d.ts +2 -2
  65. package/dist/runner/index.d.ts.map +1 -1
  66. package/dist/runner/index.js +1 -1
  67. package/dist/runner/index.js.map +1 -1
  68. package/dist/runner/runner.d.ts +9 -5
  69. package/dist/runner/runner.d.ts.map +1 -1
  70. package/dist/runner/runner.js +95 -22
  71. package/dist/runner/runner.js.map +1 -1
  72. package/dist/runner.d.ts +2 -2
  73. package/dist/runner.d.ts.map +1 -1
  74. package/dist/runner.js +1 -1
  75. package/dist/runner.js.map +1 -1
  76. package/dist/showcase/annotate.d.ts +28 -0
  77. package/dist/showcase/annotate.d.ts.map +1 -0
  78. package/dist/showcase/annotate.js +95 -0
  79. package/dist/showcase/annotate.js.map +1 -0
  80. package/dist/showcase/assemble.d.ts +31 -0
  81. package/dist/showcase/assemble.d.ts.map +1 -0
  82. package/dist/showcase/assemble.js +107 -0
  83. package/dist/showcase/assemble.js.map +1 -0
  84. package/dist/showcase/demo-player.d.ts +53 -0
  85. package/dist/showcase/demo-player.d.ts.map +1 -0
  86. package/dist/showcase/demo-player.js +325 -0
  87. package/dist/showcase/demo-player.js.map +1 -0
  88. package/dist/showcase/index.d.ts +21 -0
  89. package/dist/showcase/index.d.ts.map +1 -0
  90. package/dist/showcase/index.js +289 -0
  91. package/dist/showcase/index.js.map +1 -0
  92. package/dist/showcase/types.d.ts +111 -0
  93. package/dist/showcase/types.d.ts.map +1 -0
  94. package/dist/showcase/types.js +3 -0
  95. package/dist/showcase/types.js.map +1 -0
  96. package/dist/test-runner.d.ts +1 -0
  97. package/dist/test-runner.d.ts.map +1 -1
  98. package/dist/test-runner.js +26 -2
  99. package/dist/test-runner.js.map +1 -1
  100. package/dist/types.d.ts +32 -0
  101. package/dist/types.d.ts.map +1 -1
  102. package/package.json +11 -1
package/dist/cli.js CHANGED
@@ -21,6 +21,8 @@ import { isPersonaId, listPersonaIds, withPersonaDirective } from './personas.js
21
21
  import { resolveProviderApiKey, resolveProviderModelName } from './provider-defaults.js';
22
22
  import { loadLocalEnvFiles } from './env-loader.js';
23
23
  import { CliRenderer, cliError, cliWarn, cliLog, printStyledHelp } from './cli-ui.js';
24
+ import { ProjectStore } from './memory/project-store.js';
25
+ import { RunRegistry } from './memory/run-registry.js';
24
26
  const RUN_MODES = ['fast-explore', 'full-evidence'];
25
27
  const DRIVER_PROFILES = ['default', 'stealth', 'benchmark-webbench', 'benchmark-webbench-stealth', 'benchmark-webvoyager'];
26
28
  async function applyStorageStateToPersistentContext(context, storageStatePath) {
@@ -94,8 +96,23 @@ async function main() {
94
96
  'storage-state': { type: 'string' },
95
97
  concurrency: { type: 'string' },
96
98
  'max-turns': { type: 'string' },
99
+ 'session-id': { type: 'string' },
100
+ 'resume-run': { type: 'string' },
101
+ 'fork-run': { type: 'string' },
97
102
  pages: { type: 'string' },
98
103
  'extract-tokens': { type: 'boolean' },
104
+ rip: { type: 'boolean' },
105
+ 'design-compare': { type: 'boolean' },
106
+ 'compare-url': { type: 'string' },
107
+ // showcase
108
+ script: { type: 'string' },
109
+ capture: { type: 'string' },
110
+ crop: { type: 'string' },
111
+ highlight: { type: 'string' },
112
+ format: { type: 'string' },
113
+ viewport: { type: 'string' },
114
+ scale: { type: 'string' },
115
+ 'color-scheme': { type: 'string' },
99
116
  'llm-timeout': { type: 'string' },
100
117
  retries: { type: 'string' },
101
118
  'retry-delay-ms': { type: 'string' },
@@ -111,6 +128,8 @@ async function main() {
111
128
  timeout: { type: 'string' },
112
129
  extension: { type: 'string', multiple: true },
113
130
  'user-data-dir': { type: 'string' },
131
+ 'profile-dir': { type: 'string' },
132
+ 'cdp-url': { type: 'string' },
114
133
  wallet: { type: 'boolean' },
115
134
  'wallet-auto-approve': { type: 'boolean' },
116
135
  'wallet-password': { type: 'string' },
@@ -155,6 +174,32 @@ async function main() {
155
174
  cliError('--url is required for design-audit.');
156
175
  process.exit(1);
157
176
  }
177
+ // --design-compare mode
178
+ if (values['design-compare']) {
179
+ if (!values['compare-url']) {
180
+ cliError('--compare-url is required with --design-compare.');
181
+ process.exit(1);
182
+ }
183
+ const { runDesignCompare } = await import('./design/compare.js');
184
+ await runDesignCompare({
185
+ urlA: values.url,
186
+ urlB: values['compare-url'],
187
+ headless: values.headless,
188
+ outputDir: values.sink,
189
+ });
190
+ process.exit(0);
191
+ }
192
+ // --rip mode
193
+ if (values.rip) {
194
+ const { ripSite } = await import('./design/rip.js');
195
+ await ripSite({
196
+ url: values.url,
197
+ pages: values.pages ? parseInt(values.pages) : undefined,
198
+ headless: values.headless,
199
+ outputDir: values.sink,
200
+ });
201
+ process.exit(0);
202
+ }
158
203
  const { runDesignAudit } = await import('./cli-design-audit.js');
159
204
  await runDesignAudit({
160
205
  url: values.url,
@@ -171,13 +216,64 @@ async function main() {
171
216
  });
172
217
  process.exit(0);
173
218
  }
219
+ if (command === 'runs') {
220
+ const store = new ProjectStore(values['memory-dir']);
221
+ const registry = new RunRegistry(store.getRoot());
222
+ const runs = registry.listRuns({
223
+ domain: values.url ? new URL(values.url).hostname : undefined,
224
+ sessionId: values['session-id'],
225
+ limit: 20,
226
+ });
227
+ if (runs.length === 0) {
228
+ console.log(' No runs found.');
229
+ }
230
+ else if (values.json) {
231
+ console.log(JSON.stringify(runs, null, 2));
232
+ }
233
+ else {
234
+ for (const r of runs) {
235
+ const icon = r.status === 'completed' ? (r.success ? '\u2713' : '\u2717') : '\u25cb';
236
+ const ts = r.startedAt.slice(0, 16).replace('T', ' ');
237
+ const dur = r.completedAt
238
+ ? `${Math.round((new Date(r.completedAt).getTime() - new Date(r.startedAt).getTime()) / 1000)}s`
239
+ : 'running';
240
+ const session = r.sessionId ? ` [${r.sessionId}]` : '';
241
+ const parent = r.parentRunId ? ` \u2190 ${r.parentRunId.slice(0, 20)}` : '';
242
+ console.log(` ${icon} ${r.runId.slice(0, 30)} ${ts} ${dur} ${r.goal.slice(0, 50)}${session}${parent}`);
243
+ if (r.summary)
244
+ console.log(` ${r.summary.slice(0, 80)}`);
245
+ if (r.finalUrl)
246
+ console.log(` ${r.finalUrl}`);
247
+ }
248
+ }
249
+ process.exit(0);
250
+ }
251
+ if (command === 'showcase') {
252
+ const { handleShowcase } = await import('./cli-showcase.js');
253
+ await handleShowcase({
254
+ url: values.url,
255
+ script: values.script,
256
+ capture: values.capture,
257
+ crop: values.crop,
258
+ highlight: values.highlight,
259
+ format: values.format,
260
+ viewport: values.viewport,
261
+ output: values.sink,
262
+ headless: values.headless ?? true,
263
+ colorScheme: values['color-scheme'],
264
+ scale: values.scale ? parseFloat(values.scale) : undefined,
265
+ storageState: values['storage-state'],
266
+ quality: values['quality-threshold'] ? parseInt(values['quality-threshold']) : undefined,
267
+ });
268
+ process.exit(0);
269
+ }
174
270
  if (command !== 'run') {
175
- cliError(`Unknown command: ${command}. Use "run" or "design-audit".`);
271
+ cliError(`Unknown command: ${command}. Use "run", "runs", "design-audit", or "showcase".`);
176
272
  process.exit(1);
177
273
  }
178
274
  // Validate inputs
179
- if (!values.goal && !values.cases) {
180
- cliError('provide --goal "..." --url "..." for a single task, or --cases ./cases.json for a suite.');
275
+ if (!values.goal && !values.cases && !values['resume-run'] && !values['fork-run']) {
276
+ cliError('provide --goal "..." --url "..." for a single task, --cases ./cases.json for a suite, or --resume-run / --fork-run <runId>.');
181
277
  process.exit(1);
182
278
  }
183
279
  // Load config file, then overlay CLI flags
@@ -341,6 +437,10 @@ async function main() {
341
437
  }
342
438
  }
343
439
  }
440
+ if (values['profile-dir'])
441
+ cliOverrides.profileDir = values['profile-dir'];
442
+ if (values['cdp-url'])
443
+ cliOverrides.cdpUrl = values['cdp-url'];
344
444
  if (values.memory !== undefined || values['memory-dir']) {
345
445
  cliOverrides.memory = {
346
446
  ...(cliOverrides.memory ?? {}),
@@ -401,12 +501,8 @@ async function main() {
401
501
  cliOverrides.resourceBlocking = {
402
502
  ...(cliOverrides.resourceBlocking ?? {}),
403
503
  blockAnalytics: true,
404
- ...(profile === 'benchmark-webbench'
405
- ? {
406
- blockImages: true,
407
- blockMedia: true,
408
- }
409
- : {}),
504
+ blockImages: true,
505
+ blockMedia: true,
410
506
  };
411
507
  }
412
508
  cliOverrides.microPlan = {
@@ -517,9 +613,49 @@ async function main() {
517
613
  walletMode: Boolean(driverConfig.wallet?.enabled),
518
614
  walletAddress: driverConfig.wallet?.address,
519
615
  };
616
+ // Create project store for memory + run registry
617
+ const memoryEnabled = driverConfig.memory?.enabled === true;
618
+ const projectStore = memoryEnabled
619
+ ? new ProjectStore(driverConfig.memory?.dir)
620
+ : undefined;
621
+ const runRegistry = projectStore
622
+ ? new RunRegistry(projectStore.getRoot())
623
+ : undefined;
520
624
  // Build test cases
521
625
  let cases;
522
- if (values.cases) {
626
+ if (values['resume-run'] || values['fork-run']) {
627
+ // Resume or fork from a previous run
628
+ if (!runRegistry) {
629
+ cliError('--resume-run and --fork-run require memory to be enabled');
630
+ process.exit(1);
631
+ }
632
+ const isResume = Boolean(values['resume-run']);
633
+ const sourceRunId = (values['resume-run'] || values['fork-run']);
634
+ const scenario = isResume
635
+ ? runRegistry.buildResumeScenario(sourceRunId, values.goal)
636
+ : runRegistry.buildForkScenario(sourceRunId, values.goal || '');
637
+ if (!scenario) {
638
+ cliError(`run "${sourceRunId}" not found in registry`);
639
+ process.exit(1);
640
+ }
641
+ if (!isResume && !values.goal) {
642
+ cliError('--fork-run requires --goal');
643
+ process.exit(1);
644
+ }
645
+ cases = [{
646
+ id: `${isResume ? 'resume' : 'fork'}-${sourceRunId.slice(0, 20)}`,
647
+ name: scenario.goal.slice(0, 60),
648
+ startUrl: values.url || scenario.startUrl,
649
+ goal: scenario.goal,
650
+ allowedDomains: parseAllowedDomains(values['allowed-domains']),
651
+ maxTurns,
652
+ timeoutMs,
653
+ priority: 0,
654
+ sessionId: scenario.sessionId,
655
+ parentRunId: scenario.parentRunId,
656
+ }];
657
+ }
658
+ else if (values.cases) {
523
659
  const raw = fs.readFileSync(path.resolve(values.cases), 'utf-8');
524
660
  const parsed = JSON.parse(raw);
525
661
  const rawCases = Array.isArray(parsed) ? parsed : [parsed];
@@ -547,8 +683,14 @@ async function main() {
547
683
  maxTurns,
548
684
  timeoutMs,
549
685
  priority: 0,
686
+ sessionId: values['session-id'],
550
687
  }];
551
688
  }
689
+ // Apply --session-id to all cases from file too
690
+ if (values['session-id'] && cases.length > 0 && cases[0].id !== 'cli-task') {
691
+ const sid = values['session-id'];
692
+ cases = cases.map(c => ({ ...c, sessionId: c.sessionId || sid }));
693
+ }
552
694
  const persona = values.persona;
553
695
  if (persona) {
554
696
  if (!isPersonaId(persona)) {
@@ -594,9 +736,9 @@ async function main() {
594
736
  cliError(`storage state file not found: ${storageStatePath}`);
595
737
  process.exit(1);
596
738
  }
597
- if (launchPlan.walletMode && browserName !== 'chromium') {
598
- cliError('wallet mode currently supports Chromium only. Set --browser chromium.');
599
- process.exit(1);
739
+ if (launchPlan.persistentContext && !launchPlan.cdpUrl && browserName !== 'chromium') {
740
+ const feature = launchPlan.walletMode ? 'Wallet mode' : '--profile-dir';
741
+ throw new Error(`${feature} requires Chromium. Set --browser chromium.`);
600
742
  }
601
743
  // Ensure clean exit on interrupt
602
744
  process.on('SIGINT', () => { renderer?.destroy(); process.exit(130); });
@@ -607,145 +749,179 @@ async function main() {
607
749
  let persistentContext;
608
750
  let stopWalletAutoApprover;
609
751
  const launchDiagnostics = {};
610
- if (launchPlan.walletMode) {
752
+ // CDP connection — attach to an existing browser (Atlas, Chrome, Brave, etc.)
753
+ let cdpUrl = launchPlan.cdpUrl || process.env.BROWSER_ENDPOINT;
754
+ let cdpConnected = false;
755
+ if (cdpUrl) {
756
+ // Auto-discover WebSocket URL from HTTP endpoint
757
+ if (cdpUrl.startsWith('http://') || cdpUrl.startsWith('https://')) {
758
+ try {
759
+ const versionUrl = cdpUrl.replace(/\/$/, '') + '/json/version';
760
+ const res = await fetch(versionUrl);
761
+ const info = await res.json();
762
+ if (info.webSocketDebuggerUrl) {
763
+ if (info.Browser && !quiet)
764
+ cliLog('cdp', `connected to ${info.Browser}`);
765
+ cdpUrl = info.webSocketDebuggerUrl;
766
+ }
767
+ }
768
+ catch {
769
+ // Fall through — try the URL as-is
770
+ }
771
+ }
772
+ const cdpStartedAt = Date.now();
773
+ if (cdpUrl.includes('/devtools/') || browserName === 'chromium') {
774
+ browser = await chromium.connectOverCDP(cdpUrl);
775
+ }
776
+ else {
777
+ const browserType = browserName === 'firefox' ? firefox : browserName === 'webkit' ? webkit : chromium;
778
+ browser = await browserType.connect(cdpUrl);
779
+ }
780
+ launchDiagnostics.browserLaunchMs = Date.now() - cdpStartedAt;
781
+ launchDiagnostics.cdpUrl = cdpUrl;
782
+ cdpConnected = true;
783
+ }
784
+ else if (launchPlan.persistentContext) {
611
785
  for (const extensionPath of launchPlan.extensionPaths) {
612
786
  if (!fs.existsSync(extensionPath)) {
613
787
  throw new Error(`Wallet extension path does not exist: ${extensionPath}`);
614
788
  }
615
789
  }
616
- const userDataDir = launchPlan.userDataDir ?? path.resolve('.agent-wallet-profile');
790
+ const userDataDir = launchPlan.userDataDir ?? path.resolve(launchPlan.walletMode ? '.agent-wallet-profile' : '.agent-profile');
617
791
  fs.mkdirSync(userDataDir, { recursive: true });
618
792
  const persistentLaunchStartedAt = Date.now();
619
793
  persistentContext = await chromium.launchPersistentContext(userDataDir, {
620
794
  channel: isStealthProfile ? 'chrome' : 'chromium',
621
- headless: false,
795
+ headless: launchPlan.headless,
622
796
  args: launchPlan.browserArgs,
623
797
  viewport,
624
798
  recordVideo: { dir: videoDir, size: viewport },
625
799
  });
626
800
  launchDiagnostics.browserLaunchMs = Date.now() - persistentLaunchStartedAt;
627
801
  await applyStorageStateToPersistentContext(persistentContext, storageStatePath);
628
- const walletConfig = driverConfig.wallet ?? {};
629
- // Intercept page-level JSON-RPC so dApps see wallet balances from the
630
- // local Anvil fork. Only forward user-specific calls (eth_getBalance
631
- // for the wallet, eth_call with wallet address in calldata). Pool data
632
- // and protocol calls go to real endpoints for reliability.
633
- const walletRpcUrl = walletConfig.preflight?.chain?.rpcUrl;
634
- if (walletRpcUrl) {
635
- // Default to Anvil's first derived address if no wallet address configured
636
- const walletAddrFull = (walletConfig.address ?? '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266').toLowerCase();
637
- const walletAddrHex = walletAddrFull.replace('0x', '');
638
- await persistentContext.route('**/*', async (route) => {
639
- try {
640
- const frame = route.request().frame();
641
- if (frame && frame.url().startsWith('chrome-extension://')) {
802
+ if (launchPlan.walletMode) {
803
+ const walletConfig = driverConfig.wallet ?? {};
804
+ // Intercept page-level JSON-RPC so dApps see wallet balances from the
805
+ // local Anvil fork. Only forward user-specific calls (eth_getBalance
806
+ // for the wallet, eth_call with wallet address in calldata). Pool data
807
+ // and protocol calls go to real endpoints for reliability.
808
+ const walletRpcUrl = walletConfig.preflight?.chain?.rpcUrl;
809
+ if (walletRpcUrl) {
810
+ // Default to Anvil's first derived address if no wallet address configured
811
+ const walletAddrFull = (walletConfig.address ?? '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266').toLowerCase();
812
+ const walletAddrHex = walletAddrFull.replace('0x', '');
813
+ await persistentContext.route('**/*', async (route) => {
814
+ try {
815
+ const frame = route.request().frame();
816
+ if (frame && frame.url().startsWith('chrome-extension://')) {
817
+ await route.continue();
818
+ return;
819
+ }
820
+ }
821
+ catch {
642
822
  await route.continue();
643
823
  return;
644
824
  }
645
- }
646
- catch {
647
- await route.continue();
648
- return;
649
- }
650
- if (route.request().method() !== 'POST') {
651
- await route.continue();
652
- return;
653
- }
654
- const ct = route.request().headers()['content-type'] ?? '';
655
- if (!ct.includes('json')) {
656
- await route.continue();
657
- return;
658
- }
659
- const postData = route.request().postData();
660
- if (!postData) {
661
- await route.continue();
662
- return;
663
- }
664
- try {
665
- const body = JSON.parse(postData);
666
- const items = Array.isArray(body) ? body : [body];
667
- // Check if any item involves the wallet (balance, contract call, simulation)
668
- const isUserQuery = items.some((item) => {
669
- const method = item.method;
670
- if (!method)
671
- return false;
672
- if (method === 'eth_getBalance') {
673
- const params = item.params;
674
- return params?.[0]?.toLowerCase() === walletAddrFull;
675
- }
676
- if (method === 'eth_call' || method === 'eth_estimateGas') {
677
- const params = item.params;
678
- const txObj = params?.[0];
679
- if (!txObj)
825
+ if (route.request().method() !== 'POST') {
826
+ await route.continue();
827
+ return;
828
+ }
829
+ const ct = route.request().headers()['content-type'] ?? '';
830
+ if (!ct.includes('json')) {
831
+ await route.continue();
832
+ return;
833
+ }
834
+ const postData = route.request().postData();
835
+ if (!postData) {
836
+ await route.continue();
837
+ return;
838
+ }
839
+ try {
840
+ const body = JSON.parse(postData);
841
+ const items = Array.isArray(body) ? body : [body];
842
+ // Check if any item involves the wallet (balance, contract call, simulation)
843
+ const isUserQuery = items.some((item) => {
844
+ const method = item.method;
845
+ if (!method)
680
846
  return false;
681
- const from = txObj.from?.toLowerCase() ?? '';
682
- const data = txObj.data?.toLowerCase() ?? '';
683
- return from === walletAddrFull || data.includes(walletAddrHex);
684
- }
685
- if (method === 'eth_getTransactionCount') {
686
- const params = item.params;
687
- return params?.[0]?.toLowerCase() === walletAddrFull;
847
+ if (method === 'eth_getBalance') {
848
+ const params = item.params;
849
+ return params?.[0]?.toLowerCase() === walletAddrFull;
850
+ }
851
+ if (method === 'eth_call' || method === 'eth_estimateGas') {
852
+ const params = item.params;
853
+ const txObj = params?.[0];
854
+ if (!txObj)
855
+ return false;
856
+ const from = txObj.from?.toLowerCase() ?? '';
857
+ const data = txObj.data?.toLowerCase() ?? '';
858
+ return from === walletAddrFull || data.includes(walletAddrHex);
859
+ }
860
+ if (method === 'eth_getTransactionCount') {
861
+ const params = item.params;
862
+ return params?.[0]?.toLowerCase() === walletAddrFull;
863
+ }
864
+ return false;
865
+ });
866
+ if (!isUserQuery) {
867
+ await route.continue();
868
+ return;
688
869
  }
689
- return false;
690
- });
691
- if (!isUserQuery) {
870
+ // Normalize: some dApps (Aave) omit jsonrpc/id — Anvil requires them
871
+ let nextId = 1;
872
+ const normalized = items.map((item) => {
873
+ const out = { ...item, jsonrpc: '2.0', id: item.id ?? nextId++ };
874
+ delete out.chainId;
875
+ return out;
876
+ });
877
+ const payload = Array.isArray(body) ? normalized : normalized[0];
878
+ const res = await fetch(walletRpcUrl, {
879
+ method: 'POST',
880
+ headers: { 'Content-Type': 'application/json' },
881
+ body: JSON.stringify(payload),
882
+ });
883
+ await route.fulfill({
884
+ status: res.status,
885
+ contentType: 'application/json',
886
+ body: await res.text(),
887
+ });
888
+ }
889
+ catch {
692
890
  await route.continue();
693
- return;
694
891
  }
695
- // Normalize: some dApps (Aave) omit jsonrpc/id — Anvil requires them
696
- let nextId = 1;
697
- const normalized = items.map((item) => {
698
- const out = { ...item, jsonrpc: '2.0', id: item.id ?? nextId++ };
699
- delete out.chainId;
700
- return out;
701
- });
702
- const payload = Array.isArray(body) ? normalized : normalized[0];
703
- const res = await fetch(walletRpcUrl, {
704
- method: 'POST',
705
- headers: { 'Content-Type': 'application/json' },
706
- body: JSON.stringify(payload),
707
- });
708
- await route.fulfill({
709
- status: res.status,
710
- contentType: 'application/json',
711
- body: await res.text(),
712
- });
713
- }
714
- catch {
715
- await route.continue();
892
+ });
893
+ }
894
+ const shouldAutoApprove = walletConfig.autoApprove ?? true;
895
+ if (shouldAutoApprove) {
896
+ stopWalletAutoApprover = await startWalletAutoApprover(persistentContext, {
897
+ password: walletConfig.password,
898
+ tickMs: walletConfig.tickMs,
899
+ actionSelectors: walletConfig.actionSelectors,
900
+ });
901
+ }
902
+ const preflightEnabled = walletConfig.preflight?.enabled ?? true;
903
+ const preflightSeedUrls = walletConfig.preflight?.seedUrls && walletConfig.preflight.seedUrls.length > 0
904
+ ? walletConfig.preflight.seedUrls
905
+ : [...new Set(cases.map((testCase) => testCase.startUrl).filter(Boolean))];
906
+ if (preflightEnabled && preflightSeedUrls.length > 0) {
907
+ const preflight = await runWalletPreflight(persistentContext, {
908
+ seedUrls: preflightSeedUrls,
909
+ password: walletConfig.password,
910
+ actionSelectors: walletConfig.actionSelectors,
911
+ promptPaths: walletConfig.promptPaths,
912
+ connectSelectors: walletConfig.connectSelectors,
913
+ connectorSelectors: walletConfig.connectorSelectors,
914
+ requestAccounts: walletConfig.preflight?.requestAccounts,
915
+ accountsTimeoutMs: walletConfig.preflight?.accountsTimeoutMs,
916
+ maxChainSwitchAttempts: walletConfig.preflight?.maxChainSwitchAttempts,
917
+ chain: walletConfig.preflight?.chain,
918
+ log: quiet ? undefined : (message) => cliLog('wallet', message),
919
+ });
920
+ if (!preflight.ok) {
921
+ const failed = preflight.results.find((resultEntry) => !resultEntry.ready);
922
+ const details = failed?.details ?? 'unknown reason';
923
+ throw new Error(`Wallet preflight failed for ${preflight.failedUrl ?? 'unknown origin'} (${details})`);
716
924
  }
717
- });
718
- }
719
- const shouldAutoApprove = walletConfig.autoApprove ?? true;
720
- if (shouldAutoApprove) {
721
- stopWalletAutoApprover = await startWalletAutoApprover(persistentContext, {
722
- password: walletConfig.password,
723
- tickMs: walletConfig.tickMs,
724
- actionSelectors: walletConfig.actionSelectors,
725
- });
726
- }
727
- const preflightEnabled = walletConfig.preflight?.enabled ?? true;
728
- const preflightSeedUrls = walletConfig.preflight?.seedUrls && walletConfig.preflight.seedUrls.length > 0
729
- ? walletConfig.preflight.seedUrls
730
- : [...new Set(cases.map((testCase) => testCase.startUrl).filter(Boolean))];
731
- if (preflightEnabled && preflightSeedUrls.length > 0) {
732
- const preflight = await runWalletPreflight(persistentContext, {
733
- seedUrls: preflightSeedUrls,
734
- password: walletConfig.password,
735
- actionSelectors: walletConfig.actionSelectors,
736
- promptPaths: walletConfig.promptPaths,
737
- connectSelectors: walletConfig.connectSelectors,
738
- connectorSelectors: walletConfig.connectorSelectors,
739
- requestAccounts: walletConfig.preflight?.requestAccounts,
740
- accountsTimeoutMs: walletConfig.preflight?.accountsTimeoutMs,
741
- maxChainSwitchAttempts: walletConfig.preflight?.maxChainSwitchAttempts,
742
- chain: walletConfig.preflight?.chain,
743
- log: quiet ? undefined : (message) => cliLog('wallet', message),
744
- });
745
- if (!preflight.ok) {
746
- const failed = preflight.results.find((resultEntry) => !resultEntry.ready);
747
- const details = failed?.details ?? 'unknown reason';
748
- throw new Error(`Wallet preflight failed for ${preflight.failedUrl ?? 'unknown origin'} (${details})`);
749
925
  }
750
926
  }
751
927
  }
@@ -755,94 +931,160 @@ async function main() {
755
931
  : browserName === 'webkit'
756
932
  ? webkit
757
933
  : chromium;
758
- if (process.env.BROWSER_ENDPOINT) {
759
- const endpoint = process.env.BROWSER_ENDPOINT;
760
- const browserLaunchStartedAt = Date.now();
761
- // CDP endpoint (chrome, android-chrome) vs Playwright endpoint (firefox, webkit)
762
- if (browserName === 'chromium' || endpoint.includes('/devtools/')) {
763
- browser = await chromium.connectOverCDP(endpoint);
764
- }
765
- else {
766
- browser = await browserType.connect(endpoint);
767
- }
768
- launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
934
+ const browserLaunchStartedAt = Date.now();
935
+ browser = await browserType.launch({
936
+ headless: launchPlan.headless,
937
+ ...(browserName === 'chromium' ? { args: launchPlan.browserArgs } : {}),
938
+ // Use system Chrome for stealth profiles — real TLS/JA3 fingerprint vs bundled Chromium
939
+ ...(isStealthProfile && browserName === 'chromium' ? { channel: 'chrome' } : {}),
940
+ });
941
+ launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
942
+ }
943
+ // Headless Chromium sends "HeadlessChrome/..." in the default User-Agent.
944
+ // CDNs like Akamai reject this with ERR_HTTP2_PROTOCOL_ERROR before any JS
945
+ // stealth patches can run. Build a clean UA from the browser version.
946
+ const headlessUserAgent = launchPlan.headless && browser
947
+ ? (() => {
948
+ const ver = browser.version();
949
+ const plat = process.platform;
950
+ const platformToken = plat === 'win32'
951
+ ? 'Windows NT 10.0; Win64; x64'
952
+ : plat === 'linux'
953
+ ? 'X11; Linux x86_64'
954
+ : 'Macintosh; Intel Mac OS X 10_15_7';
955
+ return `Mozilla/5.0 (${platformToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ver} Safari/537.36`;
956
+ })()
957
+ : undefined;
958
+ const driverFactory = async () => {
959
+ const contextStartedAt = Date.now();
960
+ // CDP: reuse the browser's default context (preserves user cookies/sessions).
961
+ // Persistent context: use the already-opened persistent context.
962
+ // Default: create a fresh isolated context.
963
+ let context;
964
+ if (cdpConnected) {
965
+ // Reuse the user's existing browser context — cookies, localStorage, extensions intact
966
+ const contexts = browser.contexts();
967
+ context = contexts[0] ?? await browser.newContext({ viewport });
968
+ }
969
+ else if (persistentContext) {
970
+ context = persistentContext;
769
971
  }
770
972
  else {
771
- const browserLaunchStartedAt = Date.now();
772
- browser = await browserType.launch({
773
- headless: launchPlan.headless,
774
- ...(browserName === 'chromium' ? { args: launchPlan.browserArgs } : {}),
775
- // Use system Chrome for stealth profiles — real TLS/JA3 fingerprint vs bundled Chromium
776
- ...(isStealthProfile && browserName === 'chromium' ? { channel: 'chrome' } : {}),
973
+ context = await browser.newContext({
974
+ viewport,
975
+ recordVideo: { dir: videoDir, size: viewport },
976
+ storageState: storageStatePath,
977
+ locale: 'en-US',
978
+ timezoneId: 'America/New_York',
979
+ ...(headlessUserAgent ? { userAgent: headlessUserAgent } : {}),
980
+ extraHTTPHeaders: {
981
+ 'Accept-Language': 'en-US,en;q=0.9',
982
+ },
777
983
  });
778
- launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
779
984
  }
780
- }
781
- const driverFactory = async () => {
782
- const contextStartedAt = Date.now();
783
- const context = persistentContext ?? await browser.newContext({
784
- viewport,
785
- recordVideo: { dir: videoDir, size: viewport },
786
- storageState: storageStatePath,
787
- locale: 'en-US',
788
- timezoneId: 'America/New_York',
789
- extraHTTPHeaders: {
790
- 'Accept-Language': 'en-US,en;q=0.9',
791
- },
985
+ // Stealth patches: only for Playwright-controlled contexts (not real user browsers)
986
+ if (!cdpConnected) {
987
+ await context.addInitScript(`
988
+ // navigator.webdriver explicit override (backup for --disable-blink-features)
989
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
990
+ // navigator.plugins empty in headless, non-empty in real browsers
991
+ Object.defineProperty(navigator, 'plugins', {
992
+ get: () => [
993
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
994
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
995
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
996
+ ],
792
997
  });
793
- // Stealth: patch navigator/browser properties to reduce automation fingerprint.
794
- // This runs in the browser context (not Node), so we use a string to avoid TS issues.
795
- await context.addInitScript(`
796
- // navigator.webdriver explicit override (backup for --disable-blink-features)
797
- Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
798
- // navigator.pluginsempty in headless, non-empty in real browsers
799
- Object.defineProperty(navigator, 'plugins', {
800
- get: () => [
801
- { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
802
- { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
803
- { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
804
- ],
805
- });
806
- // navigator.languages must match Accept-Language header
807
- Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
808
- // hardware signals realistic desktop values
809
- Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
810
- Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
811
- // window.chrome — full stub matching real Chrome
812
- if (!window.chrome) window.chrome = {};
813
- if (!window.chrome.runtime) window.chrome.runtime = { id: undefined };
814
- if (!window.chrome.app) window.chrome.app = { isInstalled: false, InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' }, RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' } };
815
- if (!window.chrome.csi) window.chrome.csi = function() { return { onloadT: Date.now(), startE: Date.now(), pageT: Date.now() - performance.timing.navigationStart }; };
816
- if (!window.chrome.loadTimes) window.chrome.loadTimes = function() { return { commitLoadTime: Date.now() / 1000, connectionInfo: 'h2', finishDocumentLoadTime: Date.now() / 1000, finishLoadTime: Date.now() / 1000, firstPaintAfterLoadTime: 0, firstPaintTime: Date.now() / 1000, navigationType: 'Other', npnNegotiatedProtocol: 'h2', requestTime: Date.now() / 1000 - 0.16, startLoadTime: Date.now() / 1000 - 0.16, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: true, wasNpnNegotiated: true }; };
817
- // WebGL vendor/renderer match real GPU values
818
- try {
819
- const getParameter = WebGLRenderingContext.prototype.getParameter;
820
- WebGLRenderingContext.prototype.getParameter = function(parameter) {
821
- if (parameter === 37445) return 'Intel Inc.';
822
- if (parameter === 37446) return 'Intel Iris OpenGL Engine';
823
- return getParameter.call(this, parameter);
824
- };
825
- } catch (_) {}
826
- try {
827
- const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
828
- WebGL2RenderingContext.prototype.getParameter = function(parameter) {
829
- if (parameter === 37445) return 'Intel Inc.';
830
- if (parameter === 37446) return 'Intel Iris OpenGL Engine';
831
- return getParameter2.call(this, parameter);
832
- };
833
- } catch (_) {}
834
- // window.outerWidth/outerHeight0 in headless, match viewport in real browsers
835
- if (window.outerWidth === 0) Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
836
- if (window.outerHeight === 0) Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 85 });
837
- // Patch permissions API for notification checks
838
- try {
839
- const origQuery = navigator.permissions.query.bind(navigator.permissions);
840
- navigator.permissions.query = (params) =>
841
- params.name === 'notifications'
842
- ? Promise.resolve({ state: 'denied', onchange: null })
843
- : origQuery(params);
844
- } catch (_) {}
845
- `);
998
+ // navigator.languages must match Accept-Language header
999
+ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
1000
+ // hardware signals — realistic desktop values
1001
+ Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
1002
+ Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
1003
+ // window.chromefull stub matching real Chrome
1004
+ if (!window.chrome) window.chrome = {};
1005
+ if (!window.chrome.runtime) window.chrome.runtime = { id: undefined };
1006
+ if (!window.chrome.app) window.chrome.app = { isInstalled: false, InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' }, RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' } };
1007
+ if (!window.chrome.csi) window.chrome.csi = function() { return { onloadT: Date.now(), startE: Date.now(), pageT: Date.now() - performance.timing.navigationStart }; };
1008
+ if (!window.chrome.loadTimes) window.chrome.loadTimes = function() { return { commitLoadTime: Date.now() / 1000, connectionInfo: 'h2', finishDocumentLoadTime: Date.now() / 1000, finishLoadTime: Date.now() / 1000, firstPaintAfterLoadTime: 0, firstPaintTime: Date.now() / 1000, navigationType: 'Other', npnNegotiatedProtocol: 'h2', requestTime: Date.now() / 1000 - 0.16, startLoadTime: Date.now() / 1000 - 0.16, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: true, wasNpnNegotiated: true }; };
1009
+ // WebGL vendor/renderer — match real GPU values
1010
+ try {
1011
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
1012
+ WebGLRenderingContext.prototype.getParameter = function(parameter) {
1013
+ if (parameter === 37445) return 'Intel Inc.';
1014
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
1015
+ return getParameter.call(this, parameter);
1016
+ };
1017
+ } catch (_) {}
1018
+ try {
1019
+ const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
1020
+ WebGL2RenderingContext.prototype.getParameter = function(parameter) {
1021
+ if (parameter === 37445) return 'Intel Inc.';
1022
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
1023
+ return getParameter2.call(this, parameter);
1024
+ };
1025
+ } catch (_) {}
1026
+ // window.outerWidth/outerHeight 0 in headless, match viewport in real browsers
1027
+ if (window.outerWidth === 0) Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
1028
+ if (window.outerHeight === 0) Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 85 });
1029
+ // Patch permissions API — cover all permission types bots commonly mis-handle
1030
+ try {
1031
+ const origQuery = navigator.permissions.query.bind(navigator.permissions);
1032
+ navigator.permissions.query = (params) => {
1033
+ const deny = ['notifications', 'geolocation', 'camera', 'microphone', 'payment-handler'];
1034
+ if (deny.includes(params.name))
1035
+ return Promise.resolve({ state: 'denied', onchange: null });
1036
+ return origQuery(params);
1037
+ };
1038
+ } catch (_) {}
1039
+ // Canvas fingerprint noise add imperceptible per-session noise to canvas readback
1040
+ // so each session produces a unique fingerprint (defeats static fingerprint matching)
1041
+ try {
1042
+ const seed = Math.random() * 0xffff | 0;
1043
+ const noisify = (canvas) => {
1044
+ try {
1045
+ const ctx = canvas.getContext('2d');
1046
+ if (!ctx) return;
1047
+ const { width: w, height: h } = canvas;
1048
+ if (w === 0 || h === 0) return;
1049
+ const img = ctx.getImageData(0, 0, w, h);
1050
+ const d = img.data;
1051
+ for (let i = 0; i < d.length; i += 4) {
1052
+ // deterministic per-pixel noise from seed + position
1053
+ d[i] = d[i] ^ ((seed + i) & 1);
1054
+ }
1055
+ ctx.putImageData(img, 0, 0);
1056
+ } catch (_) {}
1057
+ };
1058
+ const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
1059
+ HTMLCanvasElement.prototype.toDataURL = function(...args) {
1060
+ noisify(this);
1061
+ return origToDataURL.apply(this, args);
1062
+ };
1063
+ const origToBlob = HTMLCanvasElement.prototype.toBlob;
1064
+ HTMLCanvasElement.prototype.toBlob = function(...args) {
1065
+ noisify(this);
1066
+ return origToBlob.apply(this, args);
1067
+ };
1068
+ } catch (_) {}
1069
+ // Fix CDP screenX/screenY bug — CDP Input.dispatchMouseEvent sets
1070
+ // screenX=clientX, screenY=clientY which never happens in real browsers.
1071
+ // Cloudflare Turnstile actively checks this. Add a per-session window
1072
+ // offset so screenX/screenY are realistic and internally consistent.
1073
+ try {
1074
+ const winX = Math.floor(Math.random() * 200) + 50;
1075
+ const winY = Math.floor(Math.random() * 100) + 50;
1076
+ const chrome = 85;
1077
+ Object.defineProperty(MouseEvent.prototype, 'screenX', {
1078
+ get() { return this.clientX + winX; },
1079
+ configurable: true,
1080
+ });
1081
+ Object.defineProperty(MouseEvent.prototype, 'screenY', {
1082
+ get() { return this.clientY + winY + chrome; },
1083
+ configurable: true,
1084
+ });
1085
+ } catch (_) {}
1086
+ `);
1087
+ }
846
1088
  const contextCreateMs = Date.now() - contextStartedAt;
847
1089
  const pageStartedAt = Date.now();
848
1090
  const page = await context.newPage();
@@ -874,8 +1116,9 @@ async function main() {
874
1116
  contextCreateMs,
875
1117
  pageCreateMs,
876
1118
  resourceBlockingSetupMs,
877
- storageStateApplied: Boolean(storageStatePath),
1119
+ storageStateApplied: Boolean(storageStatePath) && !cdpConnected,
878
1120
  persistentContext: Boolean(persistentContext),
1121
+ cdpConnected,
879
1122
  };
880
1123
  // Wrap in a Driver that properly tears down context on close
881
1124
  const wrappedDriver = {
@@ -887,7 +1130,7 @@ async function main() {
887
1130
  async close() {
888
1131
  await driver.close().catch(() => { });
889
1132
  await page.close().catch(() => { });
890
- if (!persistentContext) {
1133
+ if (!persistentContext && !cdpConnected) {
891
1134
  await context.close().catch(() => { });
892
1135
  }
893
1136
  },
@@ -905,8 +1148,9 @@ async function main() {
905
1148
  defaultTimeoutMs: timeoutMs,
906
1149
  driver: singleDriver,
907
1150
  driverFactory: concurrency > 1 ? driverFactory : undefined,
908
- enableMemory: driverConfig.memory?.enabled === true,
1151
+ enableMemory: memoryEnabled,
909
1152
  trajectoryStorePath: driverConfig.memory?.dir,
1153
+ projectStore,
910
1154
  concurrency,
911
1155
  screenshotInterval,
912
1156
  artifactSink: sink,
@@ -983,9 +1227,10 @@ async function main() {
983
1227
  if (runError) {
984
1228
  throw runError;
985
1229
  }
986
- await syncLocalBenchmarkRun(path.resolve(sinkDir), values.cases
1230
+ const runLabel = values.cases
987
1231
  ? `${path.basename(values.cases)} · cli run`
988
- : `${values.goal.slice(0, 80)} · cli run`);
1232
+ : `${(values.goal || values['resume-run'] || values['fork-run'] || 'run').slice(0, 80)} · cli run`;
1233
+ await syncLocalBenchmarkRun(path.resolve(sinkDir), runLabel);
989
1234
  process.exit((result?.summary.failed ?? 1) > 0 ? 1 : 0);
990
1235
  }
991
1236
  function parseAllowedDomains(value) {