@tangle-network/browser-agent-driver 0.6.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 (208) hide show
  1. package/LICENSE +196 -0
  2. package/README.md +149 -0
  3. package/dist/artifacts/composite-sink.d.ts +15 -0
  4. package/dist/artifacts/composite-sink.d.ts.map +1 -0
  5. package/dist/artifacts/composite-sink.js +27 -0
  6. package/dist/artifacts/composite-sink.js.map +1 -0
  7. package/dist/artifacts/filesystem-sink.d.ts +26 -0
  8. package/dist/artifacts/filesystem-sink.d.ts.map +1 -0
  9. package/dist/artifacts/filesystem-sink.js +81 -0
  10. package/dist/artifacts/filesystem-sink.js.map +1 -0
  11. package/dist/artifacts/types.d.ts +97 -0
  12. package/dist/artifacts/types.d.ts.map +1 -0
  13. package/dist/artifacts/types.js +9 -0
  14. package/dist/artifacts/types.js.map +1 -0
  15. package/dist/artifacts/webhook-sink.d.ts +71 -0
  16. package/dist/artifacts/webhook-sink.d.ts.map +1 -0
  17. package/dist/artifacts/webhook-sink.js +117 -0
  18. package/dist/artifacts/webhook-sink.js.map +1 -0
  19. package/dist/brain/index.d.ts +149 -0
  20. package/dist/brain/index.d.ts.map +1 -0
  21. package/dist/brain/index.js +1030 -0
  22. package/dist/brain/index.js.map +1 -0
  23. package/dist/browser-launch.d.ts +27 -0
  24. package/dist/browser-launch.d.ts.map +1 -0
  25. package/dist/browser-launch.js +92 -0
  26. package/dist/browser-launch.js.map +1 -0
  27. package/dist/cli.d.ts +14 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +1035 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/config.d.ts +143 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +205 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/context-budget.d.ts +14 -0
  36. package/dist/context-budget.d.ts.map +1 -0
  37. package/dist/context-budget.js +39 -0
  38. package/dist/context-budget.js.map +1 -0
  39. package/dist/design-audit.d.ts +48 -0
  40. package/dist/design-audit.d.ts.map +1 -0
  41. package/dist/design-audit.js +172 -0
  42. package/dist/design-audit.js.map +1 -0
  43. package/dist/domain-policy.d.ts +4 -0
  44. package/dist/domain-policy.d.ts.map +1 -0
  45. package/dist/domain-policy.js +69 -0
  46. package/dist/domain-policy.js.map +1 -0
  47. package/dist/drivers/block-patterns.d.ts +11 -0
  48. package/dist/drivers/block-patterns.d.ts.map +1 -0
  49. package/dist/drivers/block-patterns.js +123 -0
  50. package/dist/drivers/block-patterns.js.map +1 -0
  51. package/dist/drivers/cdp-page-state.d.ts +22 -0
  52. package/dist/drivers/cdp-page-state.d.ts.map +1 -0
  53. package/dist/drivers/cdp-page-state.js +38 -0
  54. package/dist/drivers/cdp-page-state.js.map +1 -0
  55. package/dist/drivers/cdp-snapshot.d.ts +32 -0
  56. package/dist/drivers/cdp-snapshot.d.ts.map +1 -0
  57. package/dist/drivers/cdp-snapshot.js +120 -0
  58. package/dist/drivers/cdp-snapshot.js.map +1 -0
  59. package/dist/drivers/playwright.d.ts +84 -0
  60. package/dist/drivers/playwright.d.ts.map +1 -0
  61. package/dist/drivers/playwright.js +385 -0
  62. package/dist/drivers/playwright.js.map +1 -0
  63. package/dist/drivers/snapshot.d.ts +134 -0
  64. package/dist/drivers/snapshot.d.ts.map +1 -0
  65. package/dist/drivers/snapshot.js +354 -0
  66. package/dist/drivers/snapshot.js.map +1 -0
  67. package/dist/drivers/types.d.ts +37 -0
  68. package/dist/drivers/types.d.ts.map +1 -0
  69. package/dist/drivers/types.js +5 -0
  70. package/dist/drivers/types.js.map +1 -0
  71. package/dist/env-loader.d.ts +2 -0
  72. package/dist/env-loader.d.ts.map +1 -0
  73. package/dist/env-loader.js +35 -0
  74. package/dist/env-loader.js.map +1 -0
  75. package/dist/formatters/slack.d.ts +37 -0
  76. package/dist/formatters/slack.d.ts.map +1 -0
  77. package/dist/formatters/slack.js +97 -0
  78. package/dist/formatters/slack.js.map +1 -0
  79. package/dist/index.d.ts +60 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +53 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/memory/analyzer.d.ts +67 -0
  84. package/dist/memory/analyzer.d.ts.map +1 -0
  85. package/dist/memory/analyzer.js +265 -0
  86. package/dist/memory/analyzer.js.map +1 -0
  87. package/dist/memory/knowledge.d.ts +55 -0
  88. package/dist/memory/knowledge.d.ts.map +1 -0
  89. package/dist/memory/knowledge.js +115 -0
  90. package/dist/memory/knowledge.js.map +1 -0
  91. package/dist/memory/project-store.d.ts +43 -0
  92. package/dist/memory/project-store.d.ts.map +1 -0
  93. package/dist/memory/project-store.js +119 -0
  94. package/dist/memory/project-store.js.map +1 -0
  95. package/dist/memory/selectors.d.ts +46 -0
  96. package/dist/memory/selectors.d.ts.map +1 -0
  97. package/dist/memory/selectors.js +89 -0
  98. package/dist/memory/selectors.js.map +1 -0
  99. package/dist/memory/store.d.ts +39 -0
  100. package/dist/memory/store.d.ts.map +1 -0
  101. package/dist/memory/store.js +193 -0
  102. package/dist/memory/store.js.map +1 -0
  103. package/dist/model-pricing.d.ts +41 -0
  104. package/dist/model-pricing.d.ts.map +1 -0
  105. package/dist/model-pricing.js +197 -0
  106. package/dist/model-pricing.js.map +1 -0
  107. package/dist/multi-actor.d.ts +108 -0
  108. package/dist/multi-actor.d.ts.map +1 -0
  109. package/dist/multi-actor.js +192 -0
  110. package/dist/multi-actor.js.map +1 -0
  111. package/dist/override-pipeline.d.ts +57 -0
  112. package/dist/override-pipeline.d.ts.map +1 -0
  113. package/dist/override-pipeline.js +26 -0
  114. package/dist/override-pipeline.js.map +1 -0
  115. package/dist/personas.d.ts +19 -0
  116. package/dist/personas.d.ts.map +1 -0
  117. package/dist/personas.js +86 -0
  118. package/dist/personas.js.map +1 -0
  119. package/dist/preview.d.ts +32 -0
  120. package/dist/preview.d.ts.map +1 -0
  121. package/dist/preview.js +138 -0
  122. package/dist/preview.js.map +1 -0
  123. package/dist/provider-defaults.d.ts +6 -0
  124. package/dist/provider-defaults.d.ts.map +1 -0
  125. package/dist/provider-defaults.js +40 -0
  126. package/dist/provider-defaults.js.map +1 -0
  127. package/dist/providers/sandbox-backend.d.ts +22 -0
  128. package/dist/providers/sandbox-backend.d.ts.map +1 -0
  129. package/dist/providers/sandbox-backend.js +259 -0
  130. package/dist/providers/sandbox-backend.js.map +1 -0
  131. package/dist/recovery.d.ts +91 -0
  132. package/dist/recovery.d.ts.map +1 -0
  133. package/dist/recovery.js +500 -0
  134. package/dist/recovery.js.map +1 -0
  135. package/dist/reporters/junit.d.ts +8 -0
  136. package/dist/reporters/junit.d.ts.map +1 -0
  137. package/dist/reporters/junit.js +99 -0
  138. package/dist/reporters/junit.js.map +1 -0
  139. package/dist/run-metrics.d.ts +4 -0
  140. package/dist/run-metrics.d.ts.map +1 -0
  141. package/dist/run-metrics.js +48 -0
  142. package/dist/run-metrics.js.map +1 -0
  143. package/dist/run-state.d.ts +22 -0
  144. package/dist/run-state.d.ts.map +1 -0
  145. package/dist/run-state.js +33 -0
  146. package/dist/run-state.js.map +1 -0
  147. package/dist/runner/goal-verification.d.ts +13 -0
  148. package/dist/runner/goal-verification.d.ts.map +1 -0
  149. package/dist/runner/goal-verification.js +152 -0
  150. package/dist/runner/goal-verification.js.map +1 -0
  151. package/dist/runner/index.d.ts +12 -0
  152. package/dist/runner/index.d.ts.map +1 -0
  153. package/dist/runner/index.js +18 -0
  154. package/dist/runner/index.js.map +1 -0
  155. package/dist/runner/overrides.d.ts +75 -0
  156. package/dist/runner/overrides.d.ts.map +1 -0
  157. package/dist/runner/overrides.js +385 -0
  158. package/dist/runner/overrides.js.map +1 -0
  159. package/dist/runner/page-analysis.d.ts +23 -0
  160. package/dist/runner/page-analysis.d.ts.map +1 -0
  161. package/dist/runner/page-analysis.js +91 -0
  162. package/dist/runner/page-analysis.js.map +1 -0
  163. package/dist/runner/runner.d.ts +66 -0
  164. package/dist/runner/runner.d.ts.map +1 -0
  165. package/dist/runner/runner.js +1314 -0
  166. package/dist/runner/runner.js.map +1 -0
  167. package/dist/runner/scout.d.ts +29 -0
  168. package/dist/runner/scout.d.ts.map +1 -0
  169. package/dist/runner/scout.js +81 -0
  170. package/dist/runner/scout.js.map +1 -0
  171. package/dist/runner/search-guidance.d.ts +31 -0
  172. package/dist/runner/search-guidance.d.ts.map +1 -0
  173. package/dist/runner/search-guidance.js +210 -0
  174. package/dist/runner/search-guidance.js.map +1 -0
  175. package/dist/runner/utils.d.ts +28 -0
  176. package/dist/runner/utils.d.ts.map +1 -0
  177. package/dist/runner/utils.js +119 -0
  178. package/dist/runner/utils.js.map +1 -0
  179. package/dist/runner.d.ts +8 -0
  180. package/dist/runner.d.ts.map +1 -0
  181. package/dist/runner.js +21 -0
  182. package/dist/runner.js.map +1 -0
  183. package/dist/supervisor/critic.d.ts +29 -0
  184. package/dist/supervisor/critic.d.ts.map +1 -0
  185. package/dist/supervisor/critic.js +320 -0
  186. package/dist/supervisor/critic.js.map +1 -0
  187. package/dist/supervisor/policy.d.ts +11 -0
  188. package/dist/supervisor/policy.d.ts.map +1 -0
  189. package/dist/supervisor/policy.js +118 -0
  190. package/dist/supervisor/policy.js.map +1 -0
  191. package/dist/test-report.d.ts +15 -0
  192. package/dist/test-report.d.ts.map +1 -0
  193. package/dist/test-report.js +241 -0
  194. package/dist/test-report.js.map +1 -0
  195. package/dist/test-runner.d.ts +99 -0
  196. package/dist/test-runner.d.ts.map +1 -0
  197. package/dist/test-runner.js +1075 -0
  198. package/dist/test-runner.js.map +1 -0
  199. package/dist/types.d.ts +493 -0
  200. package/dist/types.d.ts.map +1 -0
  201. package/dist/types.js +5 -0
  202. package/dist/types.js.map +1 -0
  203. package/dist/wallet/automation.d.ts +52 -0
  204. package/dist/wallet/automation.d.ts.map +1 -0
  205. package/dist/wallet/automation.js +507 -0
  206. package/dist/wallet/automation.js.map +1 -0
  207. package/package.json +101 -0
  208. package/scripts/postinstall-provider-patches.mjs +52 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1035 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI for browser-agent-driver — run agent test cases from the command line.
4
+ *
5
+ * Usage:
6
+ * bad run --goal "Sign up" --url http://localhost:3000
7
+ * bad run --cases ./cases.json --concurrency 4
8
+ * bad run --cases ./cases.json --sink ./results/ --model claude-sonnet-4-20250514
9
+ *
10
+ * Designed for sandbox/container execution:
11
+ * docker run bad run --cases /data/cases.json --sink /output/
12
+ */
13
+ import { parseArgs } from 'node:util';
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { spawn } from 'node:child_process';
17
+ import { loadConfig, mergeConfig, toAgentConfig } from './config.js';
18
+ import { buildBrowserLaunchPlan } from './browser-launch.js';
19
+ import { runWalletPreflight, startWalletAutoApprover } from './wallet/automation.js';
20
+ import { isPersonaId, listPersonaIds, withPersonaDirective } from './personas.js';
21
+ import { resolveProviderApiKey, resolveProviderModelName } from './provider-defaults.js';
22
+ import { loadLocalEnvFiles } from './env-loader.js';
23
+ const RUN_MODES = ['fast-explore', 'full-evidence'];
24
+ const DRIVER_PROFILES = ['default', 'stealth', 'benchmark-webbench', 'benchmark-webbench-stealth', 'benchmark-webvoyager'];
25
+ async function applyStorageStateToPersistentContext(context, storageStatePath) {
26
+ if (!storageStatePath)
27
+ return;
28
+ const parsed = JSON.parse(fs.readFileSync(storageStatePath, 'utf-8'));
29
+ const cookies = parsed.cookies ?? [];
30
+ if (cookies.length > 0) {
31
+ await context.addCookies(cookies);
32
+ }
33
+ const origins = parsed.origins ?? [];
34
+ if (origins.length === 0)
35
+ return;
36
+ const existingPages = context.pages();
37
+ const page = existingPages[0] ?? await context.newPage();
38
+ const createdTempPage = existingPages.length === 0;
39
+ try {
40
+ for (const originState of origins) {
41
+ if (!originState?.origin || !Array.isArray(originState.localStorage) || originState.localStorage.length === 0) {
42
+ continue;
43
+ }
44
+ await page.goto(originState.origin, { waitUntil: 'domcontentloaded' }).catch(() => { });
45
+ await page.evaluate((entries) => {
46
+ for (const entry of entries) {
47
+ try {
48
+ localStorage.setItem(entry.name, entry.value);
49
+ }
50
+ catch {
51
+ // Best effort: some origins may block storage writes.
52
+ }
53
+ }
54
+ }, originState.localStorage);
55
+ }
56
+ }
57
+ finally {
58
+ if (createdTempPage) {
59
+ await page.close().catch(() => { });
60
+ }
61
+ }
62
+ }
63
+ async function main() {
64
+ loadLocalEnvFiles(process.cwd());
65
+ const { values, positionals } = parseArgs({
66
+ allowPositionals: true,
67
+ allowNegative: true,
68
+ options: {
69
+ // Config file
70
+ config: { type: 'string' },
71
+ // Test specification
72
+ goal: { type: 'string', short: 'g' },
73
+ url: { type: 'string', short: 'u' },
74
+ cases: { type: 'string', short: 'c' },
75
+ 'allowed-domains': { type: 'string' },
76
+ // LLM configuration
77
+ model: { type: 'string', short: 'm' },
78
+ provider: { type: 'string' },
79
+ 'model-adaptive': { type: 'boolean' },
80
+ 'nav-model': { type: 'string' },
81
+ 'nav-provider': { type: 'string' },
82
+ persona: { type: 'string' },
83
+ mode: { type: 'string' },
84
+ profile: { type: 'string' },
85
+ 'prompt-file': { type: 'string' },
86
+ 'sandbox-backend-type': { type: 'string' },
87
+ 'sandbox-backend-profile': { type: 'string' },
88
+ 'sandbox-backend-provider': { type: 'string' },
89
+ 'api-key': { type: 'string' },
90
+ 'base-url': { type: 'string' },
91
+ // Execution
92
+ browser: { type: 'string' },
93
+ 'storage-state': { type: 'string' },
94
+ concurrency: { type: 'string' },
95
+ 'max-turns': { type: 'string' },
96
+ 'llm-timeout': { type: 'string' },
97
+ retries: { type: 'string' },
98
+ 'retry-delay-ms': { type: 'string' },
99
+ 'screenshot-interval': { type: 'string' },
100
+ scout: { type: 'boolean' },
101
+ 'scout-model': { type: 'string' },
102
+ 'scout-provider': { type: 'string' },
103
+ 'scout-vision': { type: 'boolean' },
104
+ 'scout-max-candidates': { type: 'string' },
105
+ 'scout-min-top-score': { type: 'string' },
106
+ 'scout-max-score-gap': { type: 'string' },
107
+ headless: { type: 'boolean' },
108
+ timeout: { type: 'string' },
109
+ extension: { type: 'string', multiple: true },
110
+ 'user-data-dir': { type: 'string' },
111
+ wallet: { type: 'boolean' },
112
+ 'wallet-auto-approve': { type: 'boolean' },
113
+ 'wallet-password': { type: 'string' },
114
+ 'wallet-seed-url': { type: 'string', multiple: true },
115
+ 'wallet-preflight': { type: 'boolean' },
116
+ 'wallet-chain-id': { type: 'string' },
117
+ 'wallet-chain-rpc-url': { type: 'string' },
118
+ memory: { type: 'boolean' },
119
+ 'memory-dir': { type: 'string' },
120
+ // Output
121
+ sink: { type: 'string', short: 's' },
122
+ json: { type: 'boolean', default: false },
123
+ quiet: { type: 'boolean', short: 'q', default: false },
124
+ // Feature flags
125
+ 'goal-verification': { type: 'boolean' },
126
+ 'quality-threshold': { type: 'string' },
127
+ 'trace-scoring': { type: 'boolean' },
128
+ 'trace-ttl-days': { type: 'string' },
129
+ vision: { type: 'boolean' },
130
+ 'vision-strategy': { type: 'string' },
131
+ debug: { type: 'boolean', short: 'd', default: false },
132
+ // Resource blocking
133
+ 'block-analytics': { type: 'boolean', default: false },
134
+ 'block-images': { type: 'boolean', default: false },
135
+ 'block-media': { type: 'boolean', default: false },
136
+ help: { type: 'boolean', short: 'h', default: false },
137
+ version: { type: 'boolean', short: 'v', default: false },
138
+ },
139
+ });
140
+ if (values.version) {
141
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
142
+ console.log(pkg.version);
143
+ process.exit(0);
144
+ }
145
+ if (values.help || positionals.length === 0) {
146
+ printHelp();
147
+ process.exit(0);
148
+ }
149
+ const command = positionals[0];
150
+ if (command !== 'run') {
151
+ console.error(`Unknown command: ${command}. Use "run".`);
152
+ process.exit(1);
153
+ }
154
+ // Validate inputs
155
+ if (!values.goal && !values.cases) {
156
+ console.error('Error: provide --goal "..." --url "..." for a single task, or --cases ./cases.json for a suite.');
157
+ process.exit(1);
158
+ }
159
+ // Load config file, then overlay CLI flags
160
+ const fileConfig = await loadConfig(values.config);
161
+ const mode = values.mode;
162
+ if (mode && !RUN_MODES.includes(mode)) {
163
+ console.error(`Error: unknown mode "${mode}". Valid modes: ${RUN_MODES.join(', ')}`);
164
+ process.exit(1);
165
+ }
166
+ const profile = values.profile;
167
+ if (profile && !DRIVER_PROFILES.includes(profile)) {
168
+ console.error(`Error: unknown profile "${profile}". Valid profiles: ${DRIVER_PROFILES.join(', ')}`);
169
+ process.exit(1);
170
+ }
171
+ // Build CLI overrides (only set values that were explicitly passed)
172
+ const cliOverrides = {};
173
+ if (values.model)
174
+ cliOverrides.model = values.model;
175
+ if (values.provider)
176
+ cliOverrides.provider = values.provider;
177
+ if (values['model-adaptive'] !== undefined)
178
+ cliOverrides.adaptiveModelRouting = values['model-adaptive'];
179
+ if (values['nav-model'])
180
+ cliOverrides.navModel = values['nav-model'];
181
+ if (values['nav-provider'])
182
+ cliOverrides.navProvider = values['nav-provider'];
183
+ if (values['sandbox-backend-type'])
184
+ cliOverrides.sandboxBackendType = values['sandbox-backend-type'];
185
+ if (values['sandbox-backend-profile'])
186
+ cliOverrides.sandboxBackendProfile = values['sandbox-backend-profile'];
187
+ if (values['sandbox-backend-provider'])
188
+ cliOverrides.sandboxBackendProvider = values['sandbox-backend-provider'];
189
+ if (values['api-key'])
190
+ cliOverrides.apiKey = values['api-key'];
191
+ if (values['base-url'])
192
+ cliOverrides.baseUrl = values['base-url'];
193
+ if (values['prompt-file']) {
194
+ const promptPath = path.resolve(values['prompt-file']);
195
+ if (!fs.existsSync(promptPath)) {
196
+ console.error(`Error: prompt file not found: ${promptPath}`);
197
+ process.exit(1);
198
+ }
199
+ cliOverrides.systemPrompt = fs.readFileSync(promptPath, 'utf-8').trim();
200
+ if (!cliOverrides.systemPrompt) {
201
+ console.error(`Error: prompt file is empty: ${promptPath}`);
202
+ process.exit(1);
203
+ }
204
+ }
205
+ if (profile)
206
+ cliOverrides.profile = profile;
207
+ if (values.browser)
208
+ cliOverrides.browser = values.browser;
209
+ if (values['storage-state'])
210
+ cliOverrides.storageState = values['storage-state'];
211
+ if (values.concurrency)
212
+ cliOverrides.concurrency = parseInt(values.concurrency, 10);
213
+ if (values['max-turns'])
214
+ cliOverrides.maxTurns = parseInt(values['max-turns'], 10);
215
+ if (values['llm-timeout'])
216
+ cliOverrides.llmTimeoutMs = parseInt(values['llm-timeout'], 10);
217
+ if (values.retries)
218
+ cliOverrides.retries = parseInt(values.retries, 10);
219
+ if (values['retry-delay-ms'])
220
+ cliOverrides.retryDelayMs = parseInt(values['retry-delay-ms'], 10);
221
+ if (values['screenshot-interval'])
222
+ cliOverrides.screenshotInterval = parseInt(values['screenshot-interval'], 10);
223
+ if (values.scout !== undefined ||
224
+ values['scout-model'] ||
225
+ values['scout-provider'] ||
226
+ values['scout-vision'] !== undefined ||
227
+ values['scout-max-candidates'] ||
228
+ values['scout-min-top-score'] ||
229
+ values['scout-max-score-gap']) {
230
+ cliOverrides.scout = {
231
+ ...(cliOverrides.scout ?? {}),
232
+ };
233
+ if (values.scout !== undefined)
234
+ cliOverrides.scout.enabled = values.scout;
235
+ if (values['scout-model'])
236
+ cliOverrides.scout.model = values['scout-model'];
237
+ if (values['scout-provider']) {
238
+ cliOverrides.scout.provider = values['scout-provider'];
239
+ }
240
+ if (values['scout-vision'] !== undefined)
241
+ cliOverrides.scout.useVision = values['scout-vision'];
242
+ if (values['scout-max-candidates'])
243
+ cliOverrides.scout.maxCandidates = parseInt(values['scout-max-candidates'], 10);
244
+ if (values['scout-min-top-score'])
245
+ cliOverrides.scout.minTopScore = parseInt(values['scout-min-top-score'], 10);
246
+ if (values['scout-max-score-gap'])
247
+ cliOverrides.scout.maxScoreGap = parseInt(values['scout-max-score-gap'], 10);
248
+ }
249
+ if (values.timeout)
250
+ cliOverrides.timeoutMs = parseInt(values.timeout, 10);
251
+ if (values['quality-threshold'])
252
+ cliOverrides.qualityThreshold = parseInt(values['quality-threshold'], 10);
253
+ if (values['trace-scoring'] !== undefined || values['trace-ttl-days']) {
254
+ cliOverrides.memory = {
255
+ ...(cliOverrides.memory ?? {}),
256
+ };
257
+ if (values['trace-scoring'] !== undefined)
258
+ cliOverrides.memory.traceScoring = values['trace-scoring'];
259
+ if (values['trace-ttl-days'])
260
+ cliOverrides.memory.traceTtlDays = parseInt(values['trace-ttl-days'], 10);
261
+ }
262
+ if (values.sink)
263
+ cliOverrides.outputDir = values.sink;
264
+ if (values.headless !== undefined)
265
+ cliOverrides.headless = values.headless;
266
+ if (values.vision !== undefined)
267
+ cliOverrides.vision = values.vision;
268
+ if (values['vision-strategy'])
269
+ cliOverrides.visionStrategy = values['vision-strategy'];
270
+ if (values['goal-verification'] !== undefined)
271
+ cliOverrides.goalVerification = values['goal-verification'];
272
+ if (values.extension?.length ||
273
+ values['user-data-dir'] ||
274
+ values.wallet !== undefined ||
275
+ values['wallet-auto-approve'] !== undefined ||
276
+ values['wallet-password'] ||
277
+ values['wallet-seed-url']?.length ||
278
+ values['wallet-preflight'] !== undefined ||
279
+ values['wallet-chain-id'] ||
280
+ values['wallet-chain-rpc-url']) {
281
+ cliOverrides.wallet = {};
282
+ if (values.extension?.length)
283
+ cliOverrides.wallet.extensionPaths = values.extension;
284
+ if (values['user-data-dir'])
285
+ cliOverrides.wallet.userDataDir = values['user-data-dir'];
286
+ if (values.wallet !== undefined)
287
+ cliOverrides.wallet.enabled = values.wallet;
288
+ if (values['wallet-auto-approve'] !== undefined) {
289
+ cliOverrides.wallet.autoApprove = values['wallet-auto-approve'];
290
+ }
291
+ if (values['wallet-password']) {
292
+ cliOverrides.wallet.password = values['wallet-password'];
293
+ }
294
+ if (values['wallet-seed-url']?.length ||
295
+ values['wallet-preflight'] !== undefined ||
296
+ values['wallet-chain-id'] ||
297
+ values['wallet-chain-rpc-url']) {
298
+ cliOverrides.wallet.preflight = {};
299
+ if (values['wallet-seed-url']?.length) {
300
+ cliOverrides.wallet.preflight.seedUrls = values['wallet-seed-url'];
301
+ }
302
+ if (values['wallet-preflight'] !== undefined) {
303
+ cliOverrides.wallet.preflight.enabled = values['wallet-preflight'];
304
+ }
305
+ if (values['wallet-chain-id'] || values['wallet-chain-rpc-url']) {
306
+ cliOverrides.wallet.preflight.chain = {};
307
+ if (values['wallet-chain-id']) {
308
+ const parsedChainId = parseInt(values['wallet-chain-id'], 10);
309
+ if (!Number.isFinite(parsedChainId)) {
310
+ throw new Error(`Invalid --wallet-chain-id value: ${values['wallet-chain-id']}`);
311
+ }
312
+ cliOverrides.wallet.preflight.chain.id = parsedChainId;
313
+ }
314
+ if (values['wallet-chain-rpc-url']) {
315
+ cliOverrides.wallet.preflight.chain.rpcUrl = values['wallet-chain-rpc-url'];
316
+ }
317
+ }
318
+ }
319
+ }
320
+ if (values.memory !== undefined || values['memory-dir']) {
321
+ cliOverrides.memory = {
322
+ ...(cliOverrides.memory ?? {}),
323
+ };
324
+ if (values.memory !== undefined)
325
+ cliOverrides.memory.enabled = values.memory;
326
+ if (values['memory-dir'])
327
+ cliOverrides.memory.dir = values['memory-dir'];
328
+ }
329
+ // Resource blocking
330
+ if (values['block-analytics'] || values['block-images'] || values['block-media']) {
331
+ cliOverrides.resourceBlocking = {};
332
+ if (values['block-analytics'])
333
+ cliOverrides.resourceBlocking.blockAnalytics = true;
334
+ if (values['block-images'])
335
+ cliOverrides.resourceBlocking.blockImages = true;
336
+ if (values['block-media'])
337
+ cliOverrides.resourceBlocking.blockMedia = true;
338
+ }
339
+ // Profile presets apply only when equivalent flags were not explicitly set.
340
+ if (profile === 'stealth') {
341
+ if (values.headless === undefined)
342
+ cliOverrides.headless = false;
343
+ if (values['goal-verification'] === undefined)
344
+ cliOverrides.goalVerification = true;
345
+ if (!values['screenshot-interval'])
346
+ cliOverrides.screenshotInterval = 2;
347
+ if (!values['block-analytics'] && !values['block-images'] && !values['block-media']) {
348
+ cliOverrides.resourceBlocking = {
349
+ ...(cliOverrides.resourceBlocking ?? {}),
350
+ blockAnalytics: true,
351
+ };
352
+ }
353
+ cliOverrides.microPlan = {
354
+ ...(cliOverrides.microPlan ?? {}),
355
+ enabled: true,
356
+ maxActionsPerTurn: cliOverrides.microPlan?.maxActionsPerTurn ?? 2,
357
+ };
358
+ }
359
+ else if (profile === 'benchmark-webbench' || profile === 'benchmark-webbench-stealth') {
360
+ if (!values['llm-timeout'])
361
+ cliOverrides.llmTimeoutMs = 20_000;
362
+ cliOverrides.compactFirstTurn = true;
363
+ if (!values.retries)
364
+ cliOverrides.retries = 1;
365
+ if (!values['retry-delay-ms'])
366
+ cliOverrides.retryDelayMs = 250;
367
+ if (values.vision === undefined)
368
+ cliOverrides.vision = false;
369
+ if (!values['screenshot-interval'])
370
+ cliOverrides.screenshotInterval = 0;
371
+ if (values['goal-verification'] === undefined)
372
+ cliOverrides.goalVerification = true;
373
+ if (profile === 'benchmark-webbench-stealth' && values.headless === undefined) {
374
+ cliOverrides.headless = false;
375
+ }
376
+ if (!values['block-analytics'] && !values['block-images'] && !values['block-media']) {
377
+ cliOverrides.resourceBlocking = {
378
+ ...(cliOverrides.resourceBlocking ?? {}),
379
+ blockAnalytics: true,
380
+ ...(profile === 'benchmark-webbench'
381
+ ? {
382
+ blockImages: true,
383
+ blockMedia: true,
384
+ }
385
+ : {}),
386
+ };
387
+ }
388
+ cliOverrides.microPlan = {
389
+ ...(cliOverrides.microPlan ?? {}),
390
+ enabled: true,
391
+ maxActionsPerTurn: cliOverrides.microPlan?.maxActionsPerTurn ?? 2,
392
+ };
393
+ }
394
+ else if (profile === 'benchmark-webvoyager') {
395
+ if (values.vision === undefined)
396
+ cliOverrides.vision = true;
397
+ if (!values['screenshot-interval'])
398
+ cliOverrides.screenshotInterval = 2;
399
+ if (values['goal-verification'] === undefined)
400
+ cliOverrides.goalVerification = true;
401
+ cliOverrides.microPlan = {
402
+ ...(cliOverrides.microPlan ?? {}),
403
+ enabled: true,
404
+ maxActionsPerTurn: cliOverrides.microPlan?.maxActionsPerTurn ?? 2,
405
+ };
406
+ }
407
+ // Mode presets apply only when equivalent flags were not explicitly set.
408
+ if (mode === 'fast-explore') {
409
+ if (values.vision === undefined)
410
+ cliOverrides.vision = false;
411
+ if (!values['screenshot-interval'])
412
+ cliOverrides.screenshotInterval = 0;
413
+ if (values['goal-verification'] === undefined)
414
+ cliOverrides.goalVerification = true;
415
+ if (values['quality-threshold'] === undefined)
416
+ cliOverrides.qualityThreshold = 0;
417
+ if (!values['block-analytics'] && !values['block-images'] && !values['block-media']) {
418
+ cliOverrides.resourceBlocking = {
419
+ ...(cliOverrides.resourceBlocking ?? {}),
420
+ blockAnalytics: true,
421
+ };
422
+ }
423
+ }
424
+ else if (mode === 'full-evidence') {
425
+ if (values.vision === undefined)
426
+ cliOverrides.vision = true;
427
+ if (!values['screenshot-interval'])
428
+ cliOverrides.screenshotInterval = 3;
429
+ if (values['goal-verification'] === undefined)
430
+ cliOverrides.goalVerification = true;
431
+ }
432
+ const driverConfig = mergeConfig(fileConfig, cliOverrides);
433
+ const launchPlan = buildBrowserLaunchPlan(driverConfig);
434
+ const quiet = values.quiet;
435
+ for (const warning of launchPlan.warnings) {
436
+ if (!quiet) {
437
+ console.warn(`Warning: ${warning}`);
438
+ }
439
+ }
440
+ if (launchPlan.errors.length > 0) {
441
+ for (const error of launchPlan.errors) {
442
+ console.error(`Error: ${error}`);
443
+ }
444
+ process.exit(1);
445
+ }
446
+ // Dynamic imports — keeps startup fast and allows tree-shaking.
447
+ // Use patchright (Playwright fork with CDP leak fixes) for stealth profiles
448
+ // to avoid Cloudflare/DataDome detection via Runtime.enable.
449
+ const isStealthProfile = launchPlan.profile.includes('stealth');
450
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
451
+ const { chromium, firefox, webkit } = (isStealthProfile
452
+ ? await import('patchright')
453
+ : await import('playwright'));
454
+ const { PlaywrightDriver } = await import('./drivers/playwright.js');
455
+ const { TestRunner } = await import('./test-runner.js');
456
+ const { FilesystemSink } = await import('./artifacts/filesystem-sink.js');
457
+ const concurrency = launchPlan.concurrency;
458
+ const maxTurns = driverConfig.maxTurns ?? 30;
459
+ const screenshotInterval = driverConfig.screenshotInterval ?? 5;
460
+ const timeoutMs = driverConfig.timeoutMs ?? 600_000;
461
+ const browserName = driverConfig.browser ?? 'chromium';
462
+ const debug = values.debug;
463
+ const sinkDir = driverConfig.outputDir ?? './agent-results';
464
+ const resolvedProvider = driverConfig.provider || 'openai';
465
+ const resolvedApiKey = resolveProviderApiKey(resolvedProvider, driverConfig.apiKey);
466
+ const resolvedModel = resolveProviderModelName(resolvedProvider, driverConfig.model, {
467
+ sandboxBackendType: resolvedProvider === 'sandbox-backend' ? driverConfig.sandboxBackendType : undefined,
468
+ });
469
+ const resolvedNavProvider = driverConfig.navProvider || resolvedProvider;
470
+ const resolvedNavModel = driverConfig.navModel
471
+ ? resolveProviderModelName(resolvedNavProvider, driverConfig.navModel, {
472
+ sandboxBackendType: resolvedNavProvider === 'sandbox-backend' ? driverConfig.sandboxBackendType : undefined,
473
+ })
474
+ : undefined;
475
+ const resolvedSupervisorProvider = driverConfig.supervisor?.provider || resolvedProvider;
476
+ const resolvedSupervisorModel = resolveProviderModelName(resolvedSupervisorProvider, driverConfig.supervisor?.model || resolvedModel, {
477
+ sandboxBackendType: resolvedSupervisorProvider === 'sandbox-backend' ? driverConfig.sandboxBackendType : undefined,
478
+ });
479
+ const config = {
480
+ ...toAgentConfig(driverConfig),
481
+ model: resolvedModel,
482
+ ...(resolvedNavModel ? { navModel: resolvedNavModel } : {}),
483
+ apiKey: resolvedApiKey,
484
+ baseUrl: driverConfig.baseUrl || process.env.LLM_BASE_URL,
485
+ supervisor: driverConfig.supervisor
486
+ ? {
487
+ ...driverConfig.supervisor,
488
+ provider: resolvedSupervisorProvider,
489
+ model: resolvedSupervisorModel,
490
+ }
491
+ : undefined,
492
+ debug,
493
+ };
494
+ // Build test cases
495
+ let cases;
496
+ if (values.cases) {
497
+ const raw = fs.readFileSync(path.resolve(values.cases), 'utf-8');
498
+ const parsed = JSON.parse(raw);
499
+ const rawCases = Array.isArray(parsed) ? parsed : [parsed];
500
+ // Ensure required fields — spread raw case first so explicit fields become defaults
501
+ cases = rawCases.map((c, i) => ({
502
+ id: c.id || `case-${i}`,
503
+ name: c.name || c.goal?.slice(0, 60) || `Case ${i}`,
504
+ startUrl: c.startUrl || c.url || values.url || '',
505
+ goal: c.goal || '',
506
+ allowedDomains: Array.isArray(c.allowedDomains)
507
+ ? c.allowedDomains.filter((domain) => typeof domain === 'string' && domain.length > 0)
508
+ : parseAllowedDomains(values['allowed-domains']),
509
+ maxTurns: c.maxTurns || maxTurns,
510
+ timeoutMs: c.timeoutMs || timeoutMs,
511
+ priority: c.priority ?? i,
512
+ }));
513
+ }
514
+ else {
515
+ cases = [{
516
+ id: 'cli-task',
517
+ name: values.goal.slice(0, 60),
518
+ startUrl: values.url || '',
519
+ goal: values.goal,
520
+ allowedDomains: parseAllowedDomains(values['allowed-domains']),
521
+ maxTurns,
522
+ timeoutMs,
523
+ priority: 0,
524
+ }];
525
+ }
526
+ const persona = values.persona;
527
+ if (persona) {
528
+ if (!isPersonaId(persona)) {
529
+ console.error(`Error: unknown persona "${persona}". ` +
530
+ `Valid personas: ${listPersonaIds().join(', ')}`);
531
+ process.exit(1);
532
+ }
533
+ cases = cases.map((c) => ({
534
+ ...c,
535
+ goal: withPersonaDirective({
536
+ persona,
537
+ goal: c.goal,
538
+ startUrl: c.startUrl,
539
+ }),
540
+ }));
541
+ }
542
+ if (!quiet && !values.json) {
543
+ console.log(`bad v${JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version}`);
544
+ console.log(`Model: ${config.provider}/${config.model} | Browser: ${browserName} | Tests: ${cases.length} | Concurrency: ${concurrency}`);
545
+ if (mode)
546
+ console.log(`Mode: ${mode}`);
547
+ if (driverConfig.profile && driverConfig.profile !== 'default') {
548
+ console.log(`Profile: ${driverConfig.profile}`);
549
+ }
550
+ if (config.adaptiveModelRouting) {
551
+ const effectiveNavModel = config.navModel || 'gpt-4.1-mini';
552
+ console.log(`Adaptive routing: ON (nav=${config.navProvider || config.provider}/${effectiveNavModel})`);
553
+ }
554
+ console.log(`Output: ${sinkDir}`);
555
+ console.log('');
556
+ }
557
+ // Set up artifact sink
558
+ const sink = new FilesystemSink(path.resolve(sinkDir));
559
+ const videoDir = path.join(sinkDir, '_videos');
560
+ const viewport = launchPlan.viewport;
561
+ const storageStatePath = driverConfig.storageState
562
+ ? path.resolve(driverConfig.storageState)
563
+ : undefined;
564
+ if (storageStatePath && !fs.existsSync(storageStatePath)) {
565
+ console.error(`Error: storage state file not found: ${storageStatePath}`);
566
+ process.exit(1);
567
+ }
568
+ if (launchPlan.walletMode && browserName !== 'chromium') {
569
+ console.error('Error: wallet mode currently supports Chromium only. Set --browser chromium.');
570
+ process.exit(1);
571
+ }
572
+ // Set up browser
573
+ let browser;
574
+ let persistentContext;
575
+ let stopWalletAutoApprover;
576
+ const launchDiagnostics = {};
577
+ if (launchPlan.walletMode) {
578
+ for (const extensionPath of launchPlan.extensionPaths) {
579
+ if (!fs.existsSync(extensionPath)) {
580
+ throw new Error(`Wallet extension path does not exist: ${extensionPath}`);
581
+ }
582
+ }
583
+ const userDataDir = launchPlan.userDataDir ?? path.resolve('.agent-wallet-profile');
584
+ fs.mkdirSync(userDataDir, { recursive: true });
585
+ const persistentLaunchStartedAt = Date.now();
586
+ persistentContext = await chromium.launchPersistentContext(userDataDir, {
587
+ channel: isStealthProfile ? 'chrome' : 'chromium',
588
+ headless: false,
589
+ args: launchPlan.browserArgs,
590
+ viewport,
591
+ recordVideo: { dir: videoDir, size: viewport },
592
+ });
593
+ launchDiagnostics.browserLaunchMs = Date.now() - persistentLaunchStartedAt;
594
+ await applyStorageStateToPersistentContext(persistentContext, storageStatePath);
595
+ const walletConfig = driverConfig.wallet ?? {};
596
+ const shouldAutoApprove = walletConfig.autoApprove ?? true;
597
+ if (shouldAutoApprove) {
598
+ stopWalletAutoApprover = await startWalletAutoApprover(persistentContext, {
599
+ password: walletConfig.password,
600
+ tickMs: walletConfig.tickMs,
601
+ actionSelectors: walletConfig.actionSelectors,
602
+ });
603
+ }
604
+ const preflightEnabled = walletConfig.preflight?.enabled ?? true;
605
+ const preflightSeedUrls = walletConfig.preflight?.seedUrls && walletConfig.preflight.seedUrls.length > 0
606
+ ? walletConfig.preflight.seedUrls
607
+ : [...new Set(cases.map((testCase) => testCase.startUrl).filter(Boolean))];
608
+ if (preflightEnabled && preflightSeedUrls.length > 0) {
609
+ const preflight = await runWalletPreflight(persistentContext, {
610
+ seedUrls: preflightSeedUrls,
611
+ password: walletConfig.password,
612
+ actionSelectors: walletConfig.actionSelectors,
613
+ promptPaths: walletConfig.promptPaths,
614
+ connectSelectors: walletConfig.connectSelectors,
615
+ connectorSelectors: walletConfig.connectorSelectors,
616
+ requestAccounts: walletConfig.preflight?.requestAccounts,
617
+ accountsTimeoutMs: walletConfig.preflight?.accountsTimeoutMs,
618
+ maxChainSwitchAttempts: walletConfig.preflight?.maxChainSwitchAttempts,
619
+ chain: walletConfig.preflight?.chain,
620
+ log: quiet ? undefined : (message) => console.log(`[wallet] ${message}`),
621
+ });
622
+ if (!preflight.ok) {
623
+ const failed = preflight.results.find((resultEntry) => !resultEntry.ready);
624
+ const details = failed?.details ?? 'unknown reason';
625
+ throw new Error(`Wallet preflight failed for ${preflight.failedUrl ?? 'unknown origin'} (${details})`);
626
+ }
627
+ }
628
+ }
629
+ else {
630
+ const browserType = browserName === 'firefox'
631
+ ? firefox
632
+ : browserName === 'webkit'
633
+ ? webkit
634
+ : chromium;
635
+ if (process.env.BROWSER_ENDPOINT) {
636
+ const endpoint = process.env.BROWSER_ENDPOINT;
637
+ const browserLaunchStartedAt = Date.now();
638
+ // CDP endpoint (chrome, android-chrome) vs Playwright endpoint (firefox, webkit)
639
+ if (browserName === 'chromium' || endpoint.includes('/devtools/')) {
640
+ browser = await chromium.connectOverCDP(endpoint);
641
+ }
642
+ else {
643
+ browser = await browserType.connect(endpoint);
644
+ }
645
+ launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
646
+ }
647
+ else {
648
+ const browserLaunchStartedAt = Date.now();
649
+ browser = await browserType.launch({
650
+ headless: launchPlan.headless,
651
+ ...(browserName === 'chromium' ? { args: launchPlan.browserArgs } : {}),
652
+ // Use system Chrome for stealth profiles — real TLS/JA3 fingerprint vs bundled Chromium
653
+ ...(isStealthProfile && browserName === 'chromium' ? { channel: 'chrome' } : {}),
654
+ });
655
+ launchDiagnostics.browserLaunchMs = Date.now() - browserLaunchStartedAt;
656
+ }
657
+ }
658
+ const driverFactory = async () => {
659
+ const contextStartedAt = Date.now();
660
+ const context = persistentContext ?? await browser.newContext({
661
+ viewport,
662
+ recordVideo: { dir: videoDir, size: viewport },
663
+ storageState: storageStatePath,
664
+ locale: 'en-US',
665
+ timezoneId: 'America/New_York',
666
+ extraHTTPHeaders: {
667
+ 'Accept-Language': 'en-US,en;q=0.9',
668
+ },
669
+ });
670
+ // Stealth: patch navigator/browser properties to reduce automation fingerprint.
671
+ // This runs in the browser context (not Node), so we use a string to avoid TS issues.
672
+ await context.addInitScript(`
673
+ // navigator.webdriver — explicit override (backup for --disable-blink-features)
674
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
675
+ // navigator.plugins — empty in headless, non-empty in real browsers
676
+ Object.defineProperty(navigator, 'plugins', {
677
+ get: () => [
678
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
679
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
680
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
681
+ ],
682
+ });
683
+ // navigator.languages — must match Accept-Language header
684
+ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
685
+ // hardware signals — realistic desktop values
686
+ Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
687
+ Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
688
+ // window.chrome — full stub matching real Chrome
689
+ if (!window.chrome) window.chrome = {};
690
+ if (!window.chrome.runtime) window.chrome.runtime = { id: undefined };
691
+ 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' } };
692
+ if (!window.chrome.csi) window.chrome.csi = function() { return { onloadT: Date.now(), startE: Date.now(), pageT: Date.now() - performance.timing.navigationStart }; };
693
+ 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 }; };
694
+ // WebGL vendor/renderer — match real GPU values
695
+ try {
696
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
697
+ WebGLRenderingContext.prototype.getParameter = function(parameter) {
698
+ if (parameter === 37445) return 'Intel Inc.';
699
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
700
+ return getParameter.call(this, parameter);
701
+ };
702
+ } catch (_) {}
703
+ try {
704
+ const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
705
+ WebGL2RenderingContext.prototype.getParameter = function(parameter) {
706
+ if (parameter === 37445) return 'Intel Inc.';
707
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
708
+ return getParameter2.call(this, parameter);
709
+ };
710
+ } catch (_) {}
711
+ // window.outerWidth/outerHeight — 0 in headless, match viewport in real browsers
712
+ if (window.outerWidth === 0) Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
713
+ if (window.outerHeight === 0) Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 85 });
714
+ // Patch permissions API for notification checks
715
+ try {
716
+ const origQuery = navigator.permissions.query.bind(navigator.permissions);
717
+ navigator.permissions.query = (params) =>
718
+ params.name === 'notifications'
719
+ ? Promise.resolve({ state: 'denied', onchange: null })
720
+ : origQuery(params);
721
+ } catch (_) {}
722
+ `);
723
+ const contextCreateMs = Date.now() - contextStartedAt;
724
+ const pageStartedAt = Date.now();
725
+ const page = await context.newPage();
726
+ const pageCreateMs = Date.now() - pageStartedAt;
727
+ // Cap per-action timeout so one stuck click can't consume the whole case budget.
728
+ // Default 30s is fine for long runs; for short cases (120s) use at most 15s.
729
+ const actionTimeout = Math.min(30_000, Math.max(5_000, Math.floor(timeoutMs / 8)));
730
+ const driver = new PlaywrightDriver(page, {
731
+ captureScreenshots: config.vision,
732
+ screenshotQuality: 50,
733
+ disableCdp: driverConfig.disableCdp,
734
+ timeout: actionTimeout,
735
+ });
736
+ // Apply resource blocking if configured
737
+ const resourceBlockingStartedAt = Date.now();
738
+ if (driverConfig.resourceBlocking) {
739
+ await driver.setupResourceBlocking(driverConfig.resourceBlocking);
740
+ }
741
+ const resourceBlockingSetupMs = driverConfig.resourceBlocking
742
+ ? Date.now() - resourceBlockingStartedAt
743
+ : 0;
744
+ const diagnostics = {
745
+ browserName,
746
+ headless: launchPlan.headless,
747
+ walletMode: launchPlan.walletMode,
748
+ browserLaunchMs: Number(launchDiagnostics.browserLaunchMs ?? 0),
749
+ contextCreateMs,
750
+ pageCreateMs,
751
+ resourceBlockingSetupMs,
752
+ storageStateApplied: Boolean(storageStatePath),
753
+ persistentContext: Boolean(persistentContext),
754
+ };
755
+ // Wrap in a Driver that properly tears down context on close
756
+ const wrappedDriver = {
757
+ observe: () => driver.observe(),
758
+ execute: (action) => driver.execute(action),
759
+ getPage: () => driver.getPage?.(),
760
+ screenshot: () => driver.screenshot(),
761
+ getDiagnostics: () => diagnostics,
762
+ async close() {
763
+ await driver.close().catch(() => { });
764
+ await page.close().catch(() => { });
765
+ if (!persistentContext) {
766
+ await context.close().catch(() => { });
767
+ }
768
+ },
769
+ };
770
+ return wrappedDriver;
771
+ };
772
+ // Create a single driver for sequential mode
773
+ let singleDriver;
774
+ if (concurrency <= 1) {
775
+ singleDriver = await driverFactory();
776
+ }
777
+ const runner = new TestRunner({
778
+ config,
779
+ defaultTimeoutMs: timeoutMs,
780
+ driver: singleDriver,
781
+ driverFactory: concurrency > 1 ? driverFactory : undefined,
782
+ enableMemory: driverConfig.memory?.enabled === true,
783
+ trajectoryStorePath: driverConfig.memory?.dir,
784
+ concurrency,
785
+ screenshotInterval,
786
+ artifactSink: sink,
787
+ onProgress: (event) => {
788
+ if (values.json) {
789
+ console.log(JSON.stringify(event));
790
+ return;
791
+ }
792
+ if (quiet)
793
+ return;
794
+ switch (event.type) {
795
+ case 'test:start':
796
+ console.log(` ▶ ${event.testName}`);
797
+ break;
798
+ case 'test:turn':
799
+ if (debug) {
800
+ const modelTag = event.modelUsed ? ` [${event.modelUsed}]` : '';
801
+ console.log(` turn ${event.turn}: ${event.action} (${event.durationMs}ms)${modelTag}`);
802
+ }
803
+ break;
804
+ case 'test:complete': {
805
+ const costStr = event.estimatedCostUsd ? `, $${event.estimatedCostUsd.toFixed(3)}` : '';
806
+ console.log(` ${event.passed ? '✓' : '✗'} ${event.testId} — ${event.verdict.slice(0, 80)} (${event.turnsUsed} turns, ${Math.round(event.durationMs / 1000)}s${costStr})`);
807
+ break;
808
+ }
809
+ case 'suite:complete': {
810
+ console.log('');
811
+ const suiteCostStr = event.totalCostUsd ? ` | $${event.totalCostUsd.toFixed(2)}` : '';
812
+ console.log(`Done: ${event.passed} passed, ${event.failed} failed, ${event.skipped} skipped (${Math.round(event.totalMs / 1000)}s${suiteCostStr})`);
813
+ if (event.manifestUri) {
814
+ console.log(`Artifacts: ${event.manifestUri}`);
815
+ }
816
+ break;
817
+ }
818
+ }
819
+ },
820
+ });
821
+ let result;
822
+ let runError;
823
+ try {
824
+ result = await runner.runSuite(cases);
825
+ // Write reports for each configured format
826
+ const { generateReport } = await import('./test-report.js');
827
+ const reporters = driverConfig.reporters ?? ['json'];
828
+ const reportDir = path.resolve(sinkDir);
829
+ fs.mkdirSync(reportDir, { recursive: true });
830
+ const formatMeta = {
831
+ json: { ext: 'json', contentType: 'application/json' },
832
+ markdown: { ext: 'md', contentType: 'text/markdown' },
833
+ html: { ext: 'html', contentType: 'text/html' },
834
+ junit: { ext: 'xml', contentType: 'application/xml' },
835
+ };
836
+ for (const format of reporters) {
837
+ const meta = formatMeta[format];
838
+ if (!meta)
839
+ continue;
840
+ try {
841
+ const report = generateReport(result, { format, includeTurns: format === 'markdown' });
842
+ const reportPath = path.join(reportDir, `report.${meta.ext}`);
843
+ fs.writeFileSync(reportPath, report);
844
+ if (!quiet && !values.json) {
845
+ console.log(`Report: ${reportPath}`);
846
+ }
847
+ }
848
+ catch {
849
+ // Report generation is best-effort
850
+ }
851
+ }
852
+ // Also write report to stdout if JSON mode
853
+ if (values.json && !quiet) {
854
+ console.log(JSON.stringify(result, null, 2));
855
+ }
856
+ }
857
+ catch (err) {
858
+ runError = err;
859
+ }
860
+ finally {
861
+ await singleDriver?.close?.().catch(() => { });
862
+ stopWalletAutoApprover?.();
863
+ await persistentContext?.close().catch(() => { });
864
+ await browser?.close().catch(() => { });
865
+ }
866
+ if (runError) {
867
+ throw runError;
868
+ }
869
+ await syncLocalBenchmarkRun(path.resolve(sinkDir), values.cases
870
+ ? `${path.basename(values.cases)} · cli run`
871
+ : `${values.goal.slice(0, 80)} · cli run`);
872
+ process.exit((result?.summary.failed ?? 1) > 0 ? 1 : 0);
873
+ }
874
+ function printHelp() {
875
+ console.log(`
876
+ bad — LLM-driven browser automation CLI
877
+
878
+ USAGE:
879
+ bad run [options]
880
+
881
+ SINGLE TASK:
882
+ bad run --goal "Sign up for account" --url http://localhost:3000
883
+ bad run -g "Build a todo app" -u http://localhost:5173 -m claude-sonnet-4-20250514
884
+ bad run --goal "Create Coinbase blueprint and verify preview" --url https://ai.tangle.tools --persona alice-blueprint-builder
885
+ bad run --goal "Create partner project and verify preview works" --url https://ai.tangle.tools --persona auto
886
+ bad run --goal "Explore key routes quickly" --url https://example.com --mode fast-explore
887
+
888
+ TEST SUITE:
889
+ bad run --cases ./cases.json --concurrency 4
890
+ bad run --cases ./cases.json --sink ./results/ --model gpt-5.4
891
+
892
+ DOCKER:
893
+ docker run -v ./cases.json:/data/cases.json -v ./out:/output \\
894
+ bad run --cases /data/cases.json --sink /output/
895
+
896
+ OPTIONS:
897
+ --config <path> Path to config file (default: auto-detect)
898
+ -g, --goal <text> Natural language goal for single task
899
+ -u, --url <url> Starting URL
900
+ -c, --cases <file> JSON file with test cases array
901
+ --allowed-domains <csv> Comma-separated host allowlist (e.g. www.nih.gov,docs.foo.com)
902
+ --vision-strategy <m> Vision policy: always, never, auto
903
+ -m, --model <name> LLM model (default: gpt-5.4)
904
+ --provider <name> LLM provider: openai, anthropic, google, codex-cli, claude-code, sandbox-backend (default: openai)
905
+ --model-adaptive Enable adaptive model routing for decide() turns
906
+ --nav-model <name> Fast navigation model for adaptive routing
907
+ --nav-provider <name> Provider for nav model (default: same as --provider)
908
+ --persona <id> Append persona directive (${listPersonaIds().join(', ')})
909
+ --mode <name> Mode preset: ${RUN_MODES.join(', ')}
910
+ --profile <name> Execution profile: ${DRIVER_PROFILES.join(', ')}
911
+ --prompt-file <path> Load system prompt from file for experimentable prompt variants
912
+ --sandbox-backend-type <type> Native sidecar backend type for --provider sandbox-backend (e.g. claude-code, codex, opencode:with-web-search)
913
+ --sandbox-backend-profile <id> Optional native backend profile/preset ID
914
+ --sandbox-backend-provider <id> Optional native backend model provider override (mainly for opencode)
915
+ --api-key <key> API key (or set OPENAI_API_KEY / ANTHROPIC_API_KEY; codex-cli can use \`codex login\`; claude-code can use \`claude login\`)
916
+ --base-url <url> Custom LLM endpoint (e.g., LiteLLM proxy)
917
+ --browser <name> Browser: chromium, firefox, webkit (default: chromium)
918
+ --storage-state <file> Playwright storage state JSON for pre-authenticated session
919
+ --concurrency <n> Parallel workers (default: 1)
920
+ --max-turns <n> Max turns per test (default: 30)
921
+ --llm-timeout <ms> Timeout per LLM call in milliseconds
922
+ --retries <n> Retries for observe/LLM/action transient failures
923
+ --retry-delay-ms <ms> Base retry backoff in milliseconds
924
+ --screenshot-interval <n> Capture every N turns (default: 5)
925
+ --scout Enable scout/ranker recommendations on ambiguous pages
926
+ --scout-model <name> Model override for scout recommendations
927
+ --scout-provider <name> Provider override for scout recommendations
928
+ --scout-vision Let scout inspect screenshots when available
929
+ --scout-max-candidates <n> Max visible link candidates sent to the scout
930
+ --scout-min-top-score <n> Skip scout when the top deterministic score is already strong
931
+ --scout-max-score-gap <n> Skip scout when the top-vs-second score gap is already wide
932
+ --headless Run browser headless (default: true)
933
+ --no-headless Show browser window
934
+ --wallet Enable wallet mode (persistent Chromium profile)
935
+ --extension <path> Load unpacked wallet/browser extension (repeatable)
936
+ --user-data-dir <dir> Persistent profile directory for wallet sessions
937
+ --wallet-auto-approve Enable extension prompt auto-approval (default: true in wallet mode)
938
+ --wallet-password <v> Wallet unlock password for auto-approval/preflight
939
+ --wallet-preflight Run wallet origin preflight before tests (default: true in wallet mode)
940
+ --wallet-seed-url <u> Preflight URL to authorize/switch-chain (repeatable)
941
+ --wallet-chain-id <n> Target chain ID for preflight switch/add
942
+ --wallet-chain-rpc-url <u> RPC URL for preflight wallet_addEthereumChain
943
+ --memory Enable trajectory memory reuse
944
+ --memory-dir <dir> Memory directory (default: .agent-memory)
945
+ --timeout <ms> Per-test timeout in ms (default: 600000)
946
+ -s, --sink <dir> Output directory for artifacts (default: ./agent-results)
947
+ --json Output progress as JSON lines (for piping)
948
+ -q, --quiet Suppress all output
949
+ --quality-threshold <n> Min quality score 1-10 (default: 0 = skip)
950
+ --trace-scoring Enable trajectory scoring for reference trace selection
951
+ --trace-ttl-days <n> Retention window for scored traces (default: 30)
952
+ --goal-verification Verify goal completion (default: true)
953
+ --no-goal-verification Skip goal verification
954
+ --vision Enable vision/screenshots (default: true)
955
+ --no-vision Disable vision
956
+ --block-analytics Block analytics/tracking scripts
957
+ --block-images Block image loading
958
+ --block-media Block media loading (video, audio)
959
+ -d, --debug Enable debug logging
960
+ -h, --help Show this help
961
+ -v, --version Show version
962
+
963
+ TEST CASES JSON FORMAT:
964
+ [
965
+ {
966
+ "id": "signup",
967
+ "name": "User signup flow",
968
+ "goal": "Create a new account with email test@example.com",
969
+ "startUrl": "http://localhost:3000/signup",
970
+ "maxTurns": 20,
971
+ "timeoutMs": 300000,
972
+ "successDescription": "Account created and redirected to dashboard"
973
+ }
974
+ ]
975
+
976
+ ENVIRONMENT VARIABLES:
977
+ OPENAI_API_KEY OpenAI API key
978
+ ANTHROPIC_API_KEY Anthropic API key
979
+ LLM_BASE_URL Custom LLM endpoint URL
980
+ CODEX_CLI_PATH Optional Codex CLI binary path for --provider codex-cli
981
+ CODEX_ALLOW_NPX Set to 0 to disable npx fallback for --provider codex-cli
982
+ CLAUDE_CODE_CLI_PATH Optional Claude CLI binary path for --provider claude-code
983
+ SANDBOX_BACKEND_TYPE Native backend type for --provider sandbox-backend
984
+ SANDBOX_BACKEND_PROFILE Native backend profile/preset for --provider sandbox-backend
985
+ SANDBOX_BACKEND_PROFILE_ID Legacy alias for SANDBOX_BACKEND_PROFILE
986
+ SANDBOX_BACKEND_MODEL_PROVIDER Native backend provider override for --provider sandbox-backend
987
+ SANDBOX_SIDECAR_URL Sidecar API URL for --provider sandbox-backend (default: http://127.0.0.1:$SIDECAR_PORT)
988
+ SANDBOX_SIDECAR_AUTH_TOKEN Sidecar API bearer token for --provider sandbox-backend
989
+ `);
990
+ }
991
+ function parseAllowedDomains(value) {
992
+ if (!value)
993
+ return undefined;
994
+ const domains = value
995
+ .split(',')
996
+ .map((entry) => entry.trim().toLowerCase())
997
+ .filter(Boolean);
998
+ return domains.length > 0 ? [...new Set(domains)] : undefined;
999
+ }
1000
+ async function syncLocalBenchmarkRun(outPath, label) {
1001
+ if (process.env.ABD_BENCHMARK_SYNC === '0')
1002
+ return;
1003
+ const importerPath = path.resolve(path.join(new URL('.', import.meta.url).pathname, '..', '..', 'abd-app', 'worker', 'scripts', 'import-local-benchmarks.mjs'));
1004
+ if (!fs.existsSync(importerPath))
1005
+ return;
1006
+ if (!fs.existsSync(outPath))
1007
+ return;
1008
+ const args = [importerPath, '--path', outPath, '--label', label];
1009
+ const userEmail = process.env.ABD_BENCHMARK_USER_EMAIL;
1010
+ if (userEmail)
1011
+ args.push('--user-email', userEmail);
1012
+ const exitCode = await new Promise((resolve) => {
1013
+ const child = spawn('node', args, {
1014
+ cwd: path.dirname(importerPath),
1015
+ env: {
1016
+ ...process.env,
1017
+ ABD_BENCHMARK_SYNC: '0',
1018
+ },
1019
+ stdio: 'inherit',
1020
+ });
1021
+ child.once('error', () => resolve(1));
1022
+ child.once('close', (code) => resolve(code ?? 1));
1023
+ });
1024
+ if (exitCode !== 0) {
1025
+ if (process.env.ABD_BENCHMARK_SYNC_STRICT === '1') {
1026
+ throw new Error(`abd-app benchmark import failed for ${outPath}`);
1027
+ }
1028
+ console.warn(`abd-app benchmark import skipped after non-zero exit for ${outPath}`);
1029
+ }
1030
+ }
1031
+ main().catch((err) => {
1032
+ console.error(err instanceof Error ? err.message : err);
1033
+ process.exit(1);
1034
+ });
1035
+ //# sourceMappingURL=cli.js.map