@dyyz1993/agent-browser 0.13.1 → 0.23.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 (125) hide show
  1. package/bin/agent-browser-linux-x64 +0 -0
  2. package/dist/__tests__/e2e/utils/test-helpers.d.ts +1 -0
  3. package/dist/__tests__/e2e/utils/test-helpers.d.ts.map +1 -1
  4. package/dist/__tests__/e2e/utils/test-helpers.js +14 -1
  5. package/dist/__tests__/e2e/utils/test-helpers.js.map +1 -1
  6. package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
  7. package/dist/__tests__/utils/parseCli.js +83 -1
  8. package/dist/__tests__/utils/parseCli.js.map +1 -1
  9. package/dist/actions.d.ts.map +1 -1
  10. package/dist/actions.js +269 -0
  11. package/dist/actions.js.map +1 -1
  12. package/dist/browser.d.ts +11 -1
  13. package/dist/browser.d.ts.map +1 -1
  14. package/dist/browser.js +63 -2
  15. package/dist/browser.js.map +1 -1
  16. package/dist/cli/commands.d.ts.map +1 -1
  17. package/dist/cli/commands.js +165 -1
  18. package/dist/cli/commands.js.map +1 -1
  19. package/dist/cli/connection.d.ts +13 -0
  20. package/dist/cli/connection.d.ts.map +1 -1
  21. package/dist/cli/connection.js +51 -1
  22. package/dist/cli/connection.js.map +1 -1
  23. package/dist/cli/help.d.ts.map +1 -1
  24. package/dist/cli/help.js +64 -1
  25. package/dist/cli/help.js.map +1 -1
  26. package/dist/cli.js +20 -1
  27. package/dist/cli.js.map +1 -1
  28. package/dist/daemon.d.ts +1 -0
  29. package/dist/daemon.d.ts.map +1 -1
  30. package/dist/daemon.js +22 -0
  31. package/dist/daemon.js.map +1 -1
  32. package/dist/diff.d.ts.map +1 -1
  33. package/dist/diff.js +1 -1
  34. package/dist/diff.js.map +1 -1
  35. package/dist/flow/exporters/index.d.ts +4 -0
  36. package/dist/flow/exporters/index.d.ts.map +1 -0
  37. package/dist/flow/exporters/index.js +3 -0
  38. package/dist/flow/exporters/index.js.map +1 -0
  39. package/dist/flow/exporters/playwright.d.ts +20 -0
  40. package/dist/flow/exporters/playwright.d.ts.map +1 -0
  41. package/dist/flow/exporters/playwright.js +175 -0
  42. package/dist/flow/exporters/playwright.js.map +1 -0
  43. package/dist/flow/exporters/python.d.ts +20 -0
  44. package/dist/flow/exporters/python.d.ts.map +1 -0
  45. package/dist/flow/exporters/python.js +163 -0
  46. package/dist/flow/exporters/python.js.map +1 -0
  47. package/dist/flow/exporters/types.d.ts +13 -0
  48. package/dist/flow/exporters/types.d.ts.map +1 -0
  49. package/dist/flow/exporters/types.js +2 -0
  50. package/dist/flow/exporters/types.js.map +1 -0
  51. package/dist/flow/flow-executor.d.ts +55 -0
  52. package/dist/flow/flow-executor.d.ts.map +1 -0
  53. package/dist/flow/flow-executor.js +1169 -0
  54. package/dist/flow/flow-executor.js.map +1 -0
  55. package/dist/flow/index.d.ts +15 -0
  56. package/dist/flow/index.d.ts.map +1 -0
  57. package/dist/flow/index.js +10 -0
  58. package/dist/flow/index.js.map +1 -0
  59. package/dist/flow/output.d.ts +11 -0
  60. package/dist/flow/output.d.ts.map +1 -0
  61. package/dist/flow/output.js +84 -0
  62. package/dist/flow/output.js.map +1 -0
  63. package/dist/flow/plugin-system.d.ts +48 -0
  64. package/dist/flow/plugin-system.d.ts.map +1 -0
  65. package/dist/flow/plugin-system.js +132 -0
  66. package/dist/flow/plugin-system.js.map +1 -0
  67. package/dist/flow/plugins/file-output-plugin.d.ts +8 -0
  68. package/dist/flow/plugins/file-output-plugin.d.ts.map +1 -0
  69. package/dist/flow/plugins/file-output-plugin.js +31 -0
  70. package/dist/flow/plugins/file-output-plugin.js.map +1 -0
  71. package/dist/flow/plugins/index.d.ts +4 -0
  72. package/dist/flow/plugins/index.d.ts.map +1 -0
  73. package/dist/flow/plugins/index.js +4 -0
  74. package/dist/flow/plugins/index.js.map +1 -0
  75. package/dist/flow/plugins/logging-plugin.d.ts +7 -0
  76. package/dist/flow/plugins/logging-plugin.d.ts.map +1 -0
  77. package/dist/flow/plugins/logging-plugin.js +40 -0
  78. package/dist/flow/plugins/logging-plugin.js.map +1 -0
  79. package/dist/flow/plugins/webhook-plugin.d.ts +7 -0
  80. package/dist/flow/plugins/webhook-plugin.d.ts.map +1 -0
  81. package/dist/flow/plugins/webhook-plugin.js +24 -0
  82. package/dist/flow/plugins/webhook-plugin.js.map +1 -0
  83. package/dist/flow/presets/index.d.ts +10 -0
  84. package/dist/flow/presets/index.d.ts.map +1 -0
  85. package/dist/flow/presets/index.js +29 -0
  86. package/dist/flow/presets/index.js.map +1 -0
  87. package/dist/flow/recorder-to-flow.d.ts +70 -0
  88. package/dist/flow/recorder-to-flow.d.ts.map +1 -0
  89. package/dist/flow/recorder-to-flow.js +392 -0
  90. package/dist/flow/recorder-to-flow.js.map +1 -0
  91. package/dist/flow/site-manager.d.ts +24 -0
  92. package/dist/flow/site-manager.d.ts.map +1 -0
  93. package/dist/flow/site-manager.js +125 -0
  94. package/dist/flow/site-manager.js.map +1 -0
  95. package/dist/flow/types.d.ts +181 -0
  96. package/dist/flow/types.d.ts.map +1 -0
  97. package/dist/flow/types.js +2 -0
  98. package/dist/flow/types.js.map +1 -0
  99. package/dist/flow/yaml-parser.d.ts +15 -0
  100. package/dist/flow/yaml-parser.d.ts.map +1 -0
  101. package/dist/flow/yaml-parser.js +214 -0
  102. package/dist/flow/yaml-parser.js.map +1 -0
  103. package/dist/protocol.d.ts.map +1 -1
  104. package/dist/protocol.js +15 -0
  105. package/dist/protocol.js.map +1 -1
  106. package/dist/recorder/inject.js +730 -332
  107. package/dist/snapshot-store.d.ts +77 -0
  108. package/dist/snapshot-store.d.ts.map +1 -0
  109. package/dist/snapshot-store.js +97 -0
  110. package/dist/snapshot-store.js.map +1 -0
  111. package/dist/snapshot.d.ts +6 -7
  112. package/dist/snapshot.d.ts.map +1 -1
  113. package/dist/snapshot.js +437 -1
  114. package/dist/snapshot.js.map +1 -1
  115. package/dist/types.d.ts +13 -1
  116. package/dist/types.d.ts.map +1 -1
  117. package/dist/types.js.map +1 -1
  118. package/dist/viewer-script.js +4 -4
  119. package/dist/viewer-script.js.map +1 -1
  120. package/package.json +7 -3
  121. package/skills/agent-browser/SKILL.md +102 -3
  122. package/bin/agent-browser-darwin-arm64 +0 -0
  123. package/bin/agent-browser-darwin-x64 +0 -0
  124. package/bin/agent-browser-linux-arm64 +0 -0
  125. package/bin/agent-browser-win32-x64.exe +0 -0
@@ -0,0 +1,1169 @@
1
+ import { executeCommand } from '../actions.js';
2
+ import { parseCliArgs } from '../__tests__/utils/parseCli.js';
3
+ import { isSuccessResponse } from '../types.js';
4
+ import { readFileSync } from 'fs';
5
+ import { getPreset } from './presets/index.js';
6
+ import { formatOutput, writeOutput } from './output.js';
7
+ export class FlowExecutor {
8
+ browser;
9
+ context;
10
+ pluginManager;
11
+ healingLog = [];
12
+ checkpointResults = [];
13
+ constructor(browser, pluginManager) {
14
+ this.browser = browser;
15
+ this.context = { variables: {}, params: {}, results: {}, pageCount: 0, currentPage: 0 };
16
+ this.pluginManager = pluginManager || null;
17
+ if (this.pluginManager) {
18
+ this.pluginManager.setBrowser(this.browser);
19
+ }
20
+ }
21
+ async execute(site, flowName, params) {
22
+ const startTime = Date.now();
23
+ const flow = site.flows[flowName];
24
+ if (!flow)
25
+ throw new Error(`Flow "${flowName}" not found in site "${site.name}"`);
26
+ this.healingLog = [];
27
+ this.checkpointResults = [];
28
+ this.context = {
29
+ variables: { baseUrl: site.baseUrl || '' },
30
+ params: this.resolveParams(flow, params),
31
+ results: {},
32
+ pageCount: 0,
33
+ currentPage: 1,
34
+ };
35
+ if (this.pluginManager) {
36
+ this.pluginManager.setContext(this.context);
37
+ this.pluginManager.setExecuteStep((step) => this.executeStep(step, []));
38
+ }
39
+ const errors = [];
40
+ if (this.pluginManager) {
41
+ await this.pluginManager.triggerHook('onFlowStart');
42
+ }
43
+ try {
44
+ await this.executeSteps(flow.steps, errors);
45
+ }
46
+ catch (err) {
47
+ errors.push({ step: 'flow', error: err instanceof Error ? err.message : String(err) });
48
+ }
49
+ const data = {};
50
+ if (flow.output) {
51
+ for (const varName of flow.output) {
52
+ data[varName] = this.context.results[varName];
53
+ }
54
+ }
55
+ if (this.pluginManager) {
56
+ await this.pluginManager.triggerHook('onFlowEnd');
57
+ await this.pluginManager.processData(data);
58
+ }
59
+ return {
60
+ success: errors.length === 0,
61
+ site: site.name,
62
+ flow: flowName,
63
+ data,
64
+ errors,
65
+ duration: Date.now() - startTime,
66
+ healingLog: this.healingLog.length > 0 ? [...this.healingLog] : undefined,
67
+ checkpointResults: this.checkpointResults.length > 0 ? [...this.checkpointResults] : undefined,
68
+ };
69
+ }
70
+ resolveParams(flow, provided) {
71
+ const resolved = {};
72
+ if (flow.params) {
73
+ for (const param of flow.params) {
74
+ if (provided[param.name] !== undefined) {
75
+ resolved[param.name] = provided[param.name];
76
+ }
77
+ else if (param.default !== undefined) {
78
+ resolved[param.name] = param.default;
79
+ }
80
+ else if (param.required) {
81
+ throw new Error(`Required parameter "${param.name}" not provided`);
82
+ }
83
+ }
84
+ }
85
+ return resolved;
86
+ }
87
+ substituteVars(str) {
88
+ return str.replace(/\$\{(\w+)\}/g, (_, key) => {
89
+ const value = this.context.params[key] ?? this.context.variables[key] ?? '';
90
+ return String(value);
91
+ });
92
+ }
93
+ async executeSteps(steps, errors) {
94
+ for (const step of steps) {
95
+ try {
96
+ if (this.pluginManager) {
97
+ await this.pluginManager.triggerHook('onStepStart', step);
98
+ }
99
+ await this.executeStepWithRetry(step, errors);
100
+ if (step.environment?.waitDomStable) {
101
+ const frame = step.inFrame ? this.browser.getFrame(step.inFrame) : this.browser.getPage();
102
+ await this.waitForDOMStable(frame, step.environment.domStableTimeout);
103
+ }
104
+ if (step.checkpoint) {
105
+ const frame = step.inFrame ? this.browser.getFrame(step.inFrame) : this.browser.getPage();
106
+ const result = await this.verifyCheckpoint(step.checkpoint, frame);
107
+ this.checkpointResults.push({ stepId: step.id, ...result });
108
+ if (!result.passed) {
109
+ console.warn(`[Checkpoint] Step "${step.id}" checkpoint failures: ${result.failures.join('; ')}`);
110
+ }
111
+ }
112
+ if (this.pluginManager) {
113
+ const result = step.outputVar ? this.context.results[step.outputVar] : undefined;
114
+ await this.pluginManager.triggerHook('onStepEnd', step, result);
115
+ }
116
+ }
117
+ catch (err) {
118
+ if (this.pluginManager) {
119
+ await this.pluginManager.triggerHook('onStepError', step);
120
+ }
121
+ errors.push({ step: step.id, error: err instanceof Error ? err.message : String(err) });
122
+ }
123
+ }
124
+ }
125
+ async executeStepWithRetry(step, errors) {
126
+ if (!step.retry) {
127
+ await this.executeStep(step, errors);
128
+ return;
129
+ }
130
+ const { maxAttempts, delayMs, strategy } = step.retry;
131
+ let lastError;
132
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
133
+ try {
134
+ await this.executeStep(step, errors);
135
+ return;
136
+ }
137
+ catch (err) {
138
+ lastError = err;
139
+ if (attempt < maxAttempts) {
140
+ const delay = strategy === 'exponential' ? delayMs * Math.pow(2, attempt - 1) : delayMs;
141
+ await new Promise((r) => setTimeout(r, delay));
142
+ }
143
+ }
144
+ }
145
+ throw lastError;
146
+ }
147
+ async executeStep(step, errors) {
148
+ switch (step.action) {
149
+ case 'navigate':
150
+ await this.executeNavigate(step);
151
+ break;
152
+ case 'click':
153
+ await this.executeClick(step);
154
+ break;
155
+ case 'fill':
156
+ await this.executeFill(step);
157
+ break;
158
+ case 'wait':
159
+ await this.executeWait(step);
160
+ break;
161
+ case 'extract':
162
+ await this.executeExtract(step);
163
+ break;
164
+ case 'paginate':
165
+ await this.executePaginate(step, errors);
166
+ break;
167
+ case 'forEach':
168
+ await this.executeForEach(step, errors);
169
+ break;
170
+ case 'condition':
171
+ await this.executeCondition(step, errors);
172
+ break;
173
+ case 'eval':
174
+ await this.executeEval(step);
175
+ break;
176
+ case 'snapshot':
177
+ await this.executeSnapshot(step);
178
+ break;
179
+ case 'scroll':
180
+ await this.executeScroll(step);
181
+ break;
182
+ case 'press':
183
+ await this.executePress(step);
184
+ break;
185
+ case 'screenshot':
186
+ await this.executeScreenshot(step);
187
+ break;
188
+ case 'scrollUntil':
189
+ await this.executeScrollUntil(step, errors);
190
+ break;
191
+ case 'clickPaginate':
192
+ await this.executeClickPaginate(step, errors);
193
+ break;
194
+ case 'forEachItem':
195
+ await this.executeForEachItem(step, errors);
196
+ break;
197
+ case 'repeatWhile':
198
+ await this.executeRepeatWhile(step, errors);
199
+ break;
200
+ case 'collectAll':
201
+ await this.executeCollectAll(step, errors);
202
+ break;
203
+ case 'detectBlocking': {
204
+ const conditions = step.blockingConditions || [];
205
+ const isBlocked = await this.detectBlocking(conditions);
206
+ this.context.variables['isBlocked'] = isBlocked;
207
+ this.context.results['blockingDetected'] = isBlocked;
208
+ break;
209
+ }
210
+ case 'humanHelp':
211
+ await this.executeHumanHelp(step);
212
+ break;
213
+ case 'waitForHuman':
214
+ await this.executeWaitForHuman(step, errors);
215
+ break;
216
+ case 'autoRecover':
217
+ await this.executeAutoRecover(step, errors);
218
+ break;
219
+ case 'captureScript':
220
+ await this.executeCaptureScript(step);
221
+ break;
222
+ case 'readCapture': {
223
+ const captureOutputVar = step.outputVar || 'capturedApiData';
224
+ const captured = await this.readCapturedData();
225
+ this.context.results[captureOutputVar] = captured;
226
+ break;
227
+ }
228
+ case 'captureAPI':
229
+ await this.executeCaptureAPI(step);
230
+ break;
231
+ case 'readAPI':
232
+ await this.executeReadAPI(step);
233
+ break;
234
+ case 'interceptRoute':
235
+ await this.executeInterceptRoute(step);
236
+ break;
237
+ case 'removeRoute':
238
+ await this.executeRemoveRoute(step);
239
+ break;
240
+ case 'smartExtract':
241
+ await this.executeSmartExtract(step, errors);
242
+ break;
243
+ case 'formatOutput': {
244
+ const format = step.outputFormat || 'json';
245
+ const data = step.outputVar ? this.context.results[step.outputVar] : this.context.results;
246
+ const content = formatOutput(data, {
247
+ format: format,
248
+ filePath: step.file,
249
+ pretty: step.pretty !== false,
250
+ dedupField: step.dedupField,
251
+ });
252
+ this.context.variables['formattedOutput'] = content;
253
+ if (step.file) {
254
+ const written = writeOutput(data, {
255
+ format: format,
256
+ filePath: step.file,
257
+ pretty: step.pretty !== false,
258
+ dedupField: step.dedupField,
259
+ });
260
+ this.context.variables['outputFile'] = written;
261
+ }
262
+ break;
263
+ }
264
+ case 'deduplicate': {
265
+ const sourceVar = step.sourceVar || step.outputVar || 'results';
266
+ const field = step.dedupField || 'url';
267
+ const data = this.context.results[sourceVar];
268
+ if (Array.isArray(data)) {
269
+ const seen = new Set();
270
+ this.context.results[sourceVar] = data.filter((item) => {
271
+ const key = item[field];
272
+ if (seen.has(key))
273
+ return false;
274
+ seen.add(key);
275
+ return true;
276
+ });
277
+ }
278
+ break;
279
+ }
280
+ default:
281
+ if (this.pluginManager && this.pluginManager.hasAction(step.action)) {
282
+ await this.pluginManager.executeAction(step.action, step);
283
+ }
284
+ else {
285
+ console.warn(`Unknown step action: ${step.action}`);
286
+ }
287
+ }
288
+ }
289
+ async executeNavigate(step) {
290
+ const url = this.substituteVars(step.url || '');
291
+ await executeCommand(parseCliArgs(['open', url]), this.browser);
292
+ if (step.waitAfter) {
293
+ await executeCommand(parseCliArgs(['wait', '--load', step.waitAfter]), this.browser);
294
+ }
295
+ else {
296
+ await new Promise((r) => setTimeout(r, 2000));
297
+ }
298
+ }
299
+ async executeClick(step) {
300
+ const selector = await this.resolveSelector(step);
301
+ const args = ['click', selector];
302
+ if (step.inFrame)
303
+ args.push('--in-frame', step.inFrame);
304
+ await executeCommand(parseCliArgs(args), this.browser);
305
+ if (step.waitAfter) {
306
+ await executeCommand(parseCliArgs(['wait', '--load', step.waitAfter]), this.browser);
307
+ }
308
+ }
309
+ async executeFill(step) {
310
+ const selector = await this.resolveSelector(step);
311
+ const value = this.substituteVars(step.value || '');
312
+ const args = ['fill', selector, value];
313
+ if (step.inFrame)
314
+ args.push('--in-frame', step.inFrame);
315
+ await executeCommand(parseCliArgs(args), this.browser);
316
+ }
317
+ async executeWait(step) {
318
+ const args = [];
319
+ if (step.inFrame)
320
+ args.push('--in-frame', step.inFrame);
321
+ if (step.waitAfter) {
322
+ args.push('--load', step.waitAfter);
323
+ await executeCommand(parseCliArgs(['wait', ...args]), this.browser);
324
+ }
325
+ else if (step.selector) {
326
+ await executeCommand(parseCliArgs(['wait', step.selector, ...args]), this.browser);
327
+ }
328
+ else if (step.timeout) {
329
+ await executeCommand(parseCliArgs(['wait', String(step.timeout), ...args]), this.browser);
330
+ }
331
+ }
332
+ async executeExtract(step) {
333
+ const container = step.container ? this.substituteVars(step.container) : 'body';
334
+ const fields = step.fields || {};
335
+ const fieldEntries = Object.entries(fields).map(([name, def]) => {
336
+ if (typeof def === 'string') {
337
+ return `${name}: el.querySelector('${def}')?.textContent?.trim() || ''`;
338
+ }
339
+ if (def.attribute) {
340
+ return `${name}: el.querySelector('${def.selector}')?.getAttribute('${def.attribute}') || ''`;
341
+ }
342
+ return `${name}: el.querySelector('${def.selector}')?.textContent?.trim() || ''`;
343
+ });
344
+ const script = [
345
+ '((() => {',
346
+ ` const containers = document.querySelectorAll('${container}');`,
347
+ ' const results = [];',
348
+ ' containers.forEach(el => {',
349
+ ` results.push({ ${fieldEntries.join(', ')} });`,
350
+ ' });',
351
+ ' return JSON.stringify(results);',
352
+ '})())',
353
+ ].join('\n');
354
+ const result = await executeCommand(parseCliArgs(['eval', script]), this.browser);
355
+ if (isSuccessResponse(result)) {
356
+ const evalResult = result.data;
357
+ if (evalResult.result) {
358
+ const parsed = JSON.parse(String(evalResult.result));
359
+ const outputVar = step.outputVar || 'extracted';
360
+ if (this.context.results[outputVar] && Array.isArray(this.context.results[outputVar])) {
361
+ this.context.results[outputVar] = [
362
+ ...this.context.results[outputVar],
363
+ ...parsed,
364
+ ];
365
+ }
366
+ else {
367
+ this.context.results[outputVar] = parsed;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ async executePaginate(step, errors) {
373
+ const maxPages = typeof step.maxPages === 'string'
374
+ ? Number(this.substituteVars(step.maxPages))
375
+ : step.maxPages || 1;
376
+ const onEachPage = step.onEachPage || [];
377
+ for (let page = 1; page <= maxPages; page++) {
378
+ this.context.currentPage = page;
379
+ this.context.pageCount = page;
380
+ if (onEachPage.length > 0) {
381
+ await this.executeSteps(onEachPage, errors);
382
+ }
383
+ if (page < maxPages && step.nextSelector) {
384
+ try {
385
+ await executeCommand(parseCliArgs(['click', step.nextSelector]), this.browser);
386
+ await new Promise((r) => setTimeout(r, 3000));
387
+ }
388
+ catch (err) {
389
+ console.warn(`Pagination stopped at page ${page}: ${err instanceof Error ? err.message : String(err)}`);
390
+ break;
391
+ }
392
+ }
393
+ }
394
+ }
395
+ async executeForEach(step, errors) {
396
+ const sourceData = (this.context.results[step.sourceVar || ''] || []);
397
+ const subSteps = step.subSteps || [];
398
+ for (let i = 0; i < sourceData.length; i++) {
399
+ this.context.variables['item'] = sourceData[i];
400
+ this.context.variables['itemIndex'] = i;
401
+ try {
402
+ await this.executeSteps(subSteps, errors);
403
+ }
404
+ catch (err) {
405
+ errors.push({
406
+ step: `${step.id}[${i}]`,
407
+ error: err instanceof Error ? err.message : String(err),
408
+ });
409
+ }
410
+ }
411
+ }
412
+ async executeCondition(step, errors) {
413
+ const condition = this.substituteVars(step.condition || '');
414
+ let result = false;
415
+ if (condition.includes('==')) {
416
+ const [left, right] = condition.split('==').map((s) => s.trim());
417
+ const leftVal = String(this.context.params[left] ?? this.context.variables[left] ?? left);
418
+ const rightVal = String(this.context.params[right] ?? this.context.variables[right] ?? right);
419
+ result = leftVal === rightVal;
420
+ }
421
+ else {
422
+ const val = this.context.params[condition] ?? this.context.variables[condition];
423
+ result = Boolean(val);
424
+ }
425
+ if (result && step.thenSteps) {
426
+ await this.executeSteps(step.thenSteps, errors);
427
+ }
428
+ else if (!result && step.elseSteps) {
429
+ await this.executeSteps(step.elseSteps, errors);
430
+ }
431
+ }
432
+ async executeEval(step) {
433
+ const script = this.substituteVars(step.value || '');
434
+ const args = ['eval', script];
435
+ if (step.inFrame)
436
+ args.push('--in-frame', step.inFrame);
437
+ const result = await executeCommand(parseCliArgs(args), this.browser);
438
+ if (step.outputVar && isSuccessResponse(result)) {
439
+ const evalResult = result.data;
440
+ let parsed = evalResult.result;
441
+ if (typeof parsed === 'string') {
442
+ try {
443
+ parsed = JSON.parse(parsed);
444
+ }
445
+ catch { }
446
+ }
447
+ this.context.results[step.outputVar] = parsed;
448
+ }
449
+ }
450
+ async executeSnapshot(step) {
451
+ const args = ['snapshot'];
452
+ if (step.selector)
453
+ args.push('--selector', step.selector);
454
+ if (step.inFrame)
455
+ args.push('--in-frame', step.inFrame);
456
+ await executeCommand(parseCliArgs(args), this.browser);
457
+ }
458
+ async executeScroll(step) {
459
+ const direction = step.value || 'down';
460
+ const amount = step.selector || '300';
461
+ const args = ['scroll', direction, amount];
462
+ if (step.inFrame)
463
+ args.push('--in-frame', step.inFrame);
464
+ await executeCommand(parseCliArgs(args), this.browser);
465
+ }
466
+ async executePress(step) {
467
+ const key = step.value || 'Enter';
468
+ const args = ['press', key];
469
+ if (step.inFrame)
470
+ args.push('--in-frame', step.inFrame);
471
+ await executeCommand(parseCliArgs(args), this.browser);
472
+ }
473
+ async executeScreenshot(step) {
474
+ const args = ['screenshot'];
475
+ if (step.selector)
476
+ args.push(step.selector);
477
+ if (step.inFrame)
478
+ args.push('--in-frame', step.inFrame);
479
+ await executeCommand(parseCliArgs(args), this.browser);
480
+ }
481
+ async executeScrollUntil(step, errors) {
482
+ const maxIterations = step.termination?.maxIterations || 50;
483
+ const noNewThreshold = step.termination?.noNewItemsCount || 3;
484
+ const scrollAmount = step.scrollAmount || 500;
485
+ const direction = step.scrollDirection || 'down';
486
+ let prevCount = 0;
487
+ let noNewCount = 0;
488
+ for (let i = 0; i < maxIterations; i++) {
489
+ const countSelector = step.extractOnEachScroll?.container || step.extractOnEachScroll?.selector || step.selector;
490
+ if (countSelector) {
491
+ const countResult = await executeCommand(parseCliArgs(['get', 'count', countSelector]), this.browser);
492
+ if (isSuccessResponse(countResult)) {
493
+ const currentCount = Number(countResult.data.count || 0);
494
+ if (currentCount === prevCount && prevCount > 0) {
495
+ noNewCount++;
496
+ if (noNewCount >= noNewThreshold)
497
+ break;
498
+ }
499
+ else {
500
+ noNewCount = 0;
501
+ prevCount = currentCount;
502
+ }
503
+ }
504
+ }
505
+ if (step.extractOnEachScroll) {
506
+ await this.executeStep(step.extractOnEachScroll, errors);
507
+ }
508
+ if (step.termination?.jsExpression) {
509
+ const checkResult = await executeCommand(parseCliArgs(['eval', step.termination.jsExpression]), this.browser);
510
+ if (isSuccessResponse(checkResult)) {
511
+ const shouldStop = checkResult.data.result;
512
+ if (shouldStop)
513
+ break;
514
+ }
515
+ }
516
+ await executeCommand(parseCliArgs(['scroll', direction, String(scrollAmount)]), this.browser);
517
+ await new Promise((r) => setTimeout(r, 500));
518
+ }
519
+ }
520
+ async executeClickPaginate(step, errors) {
521
+ const maxPages = typeof step.termination?.maxIterations === 'string'
522
+ ? Number(this.substituteVars(step.termination.maxIterations))
523
+ : step.termination?.maxIterations || 10;
524
+ const nextSelector = step.nextSelector || '';
525
+ const waitForNav = step.waitForNavigation || 'load';
526
+ for (let page = 1; page <= maxPages; page++) {
527
+ this.context.currentPage = page;
528
+ this.context.pageCount = page;
529
+ this.context.variables['currentPage'] = page;
530
+ if (step.extractBeforeClick) {
531
+ await this.executeStep(step.extractBeforeClick, errors);
532
+ }
533
+ if (page < maxPages) {
534
+ const visibleResult = await executeCommand(parseCliArgs(['is', 'visible', nextSelector]), this.browser);
535
+ if (isSuccessResponse(visibleResult)) {
536
+ const visible = visibleResult.data.visible;
537
+ if (!visible) {
538
+ break;
539
+ }
540
+ }
541
+ const enabledResult = await executeCommand(parseCliArgs(['is', 'enabled', nextSelector]), this.browser);
542
+ if (isSuccessResponse(enabledResult)) {
543
+ const enabled = enabledResult.data.enabled;
544
+ if (!enabled) {
545
+ break;
546
+ }
547
+ }
548
+ await executeCommand(parseCliArgs(['click', nextSelector]), this.browser);
549
+ await executeCommand(parseCliArgs(['wait', '--load', waitForNav]), this.browser);
550
+ await new Promise((r) => setTimeout(r, 1000));
551
+ }
552
+ }
553
+ }
554
+ async executeForEachItem(step, errors) {
555
+ const itemSelector = step.itemSelector || '';
556
+ const itemSteps = step.itemSteps || [];
557
+ const evalResult = await executeCommand(parseCliArgs([
558
+ 'eval',
559
+ `JSON.stringify(Array.from(document.querySelectorAll('${itemSelector}')).map((el, i) => ({index: i, text: el.textContent?.trim()?.substring(0, 200) || '', html: el.innerHTML?.substring(0, 500) || ''})))`,
560
+ ]), this.browser);
561
+ if (isSuccessResponse(evalResult)) {
562
+ const items = JSON.parse(String(evalResult.data.result || '[]'));
563
+ this.context.variables['totalItems'] = items.length;
564
+ for (let i = 0; i < items.length; i++) {
565
+ this.context.variables['currentItem'] = items[i];
566
+ this.context.variables['currentItemIndex'] = i;
567
+ try {
568
+ await this.executeSteps(itemSteps, errors);
569
+ }
570
+ catch (err) {
571
+ errors.push({
572
+ step: `${step.id}[${i}]`,
573
+ error: err instanceof Error ? err.message : String(err),
574
+ });
575
+ }
576
+ }
577
+ }
578
+ }
579
+ async executeRepeatWhile(step, errors) {
580
+ const maxIterations = step.termination?.maxIterations || 100;
581
+ const conditionJs = step.conditionJs || 'false';
582
+ const loopSteps = step.loopSteps || [];
583
+ for (let i = 0; i < maxIterations; i++) {
584
+ const condResult = await executeCommand(parseCliArgs(['eval', conditionJs]), this.browser);
585
+ if (isSuccessResponse(condResult)) {
586
+ const shouldContinue = condResult.data.result;
587
+ if (!shouldContinue)
588
+ break;
589
+ }
590
+ else {
591
+ break;
592
+ }
593
+ await this.executeSteps(loopSteps, errors);
594
+ }
595
+ }
596
+ async executeCollectAll(step, errors) {
597
+ const sourceVar = step.sourceVar || 'collected';
598
+ const dedupField = step.dedupField;
599
+ const maxIterations = step.termination?.maxIterations || 50;
600
+ const collectSteps = step.collectSteps || [];
601
+ let allData = [];
602
+ let prevLength = 0;
603
+ let noNewCount = 0;
604
+ for (let i = 0; i < maxIterations; i++) {
605
+ await this.executeSteps(collectSteps, errors);
606
+ const newData = this.context.results[sourceVar];
607
+ if (Array.isArray(newData)) {
608
+ if (dedupField) {
609
+ const seen = new Set(allData.map((d) => d[dedupField]));
610
+ const unique = newData.filter((d) => !seen.has(d[dedupField]));
611
+ allData = [...allData, ...unique];
612
+ }
613
+ else {
614
+ allData = [...allData, ...newData];
615
+ }
616
+ }
617
+ if (allData.length === prevLength && prevLength > 0) {
618
+ noNewCount++;
619
+ if (noNewCount >= (step.termination?.noNewItemsCount || 3))
620
+ break;
621
+ }
622
+ else {
623
+ noNewCount = 0;
624
+ prevLength = allData.length;
625
+ }
626
+ }
627
+ this.context.results[sourceVar] = allData;
628
+ }
629
+ async detectBlocking(conditions) {
630
+ for (const condition of conditions) {
631
+ if (condition.selector) {
632
+ const result = await executeCommand(parseCliArgs(['is', 'visible', condition.selector]), this.browser);
633
+ if (isSuccessResponse(result) && result.data.visible) {
634
+ return true;
635
+ }
636
+ }
637
+ if (condition.jsExpression) {
638
+ const result = await executeCommand(parseCliArgs(['eval', condition.jsExpression]), this.browser);
639
+ if (isSuccessResponse(result) && result.data.result) {
640
+ return true;
641
+ }
642
+ }
643
+ if (condition.urlPattern) {
644
+ const result = await executeCommand(parseCliArgs(['get', 'url']), this.browser);
645
+ if (isSuccessResponse(result)) {
646
+ const url = String(result.data.url || '');
647
+ if (url.includes(condition.urlPattern)) {
648
+ return true;
649
+ }
650
+ }
651
+ }
652
+ if (condition.textContains) {
653
+ const escaped = condition.textContains.replace(/'/g, "\\'");
654
+ const result = await executeCommand(parseCliArgs(['eval', `document.body.textContent.includes('${escaped}')`]), this.browser);
655
+ if (isSuccessResponse(result) && result.data.result) {
656
+ return true;
657
+ }
658
+ }
659
+ }
660
+ return false;
661
+ }
662
+ async executeHumanHelp(step) {
663
+ const intervention = step.intervention;
664
+ if (!intervention)
665
+ return;
666
+ if (intervention.screenshot) {
667
+ await executeCommand(parseCliArgs(['screenshot']), this.browser);
668
+ }
669
+ if (intervention.openViewer) {
670
+ const viewerResult = await executeCommand({ id: `viewer-${Date.now()}`, action: 'viewer' }, this.browser);
671
+ if (isSuccessResponse(viewerResult)) {
672
+ const viewerUrl = viewerResult.data.url;
673
+ console.log(`[Human Help] Viewer opened: ${viewerUrl}`);
674
+ }
675
+ }
676
+ const mode = intervention.mode || 'askAndWait';
677
+ if (mode === 'ask' || mode === 'askAndWait') {
678
+ try {
679
+ const askResult = await executeCommand({ id: `ask-${Date.now()}`, action: 'ask', question: intervention.message }, this.browser);
680
+ if (isSuccessResponse(askResult)) {
681
+ const answer = askResult.data.answer;
682
+ console.log(`[Human Help] Human responded: ${answer}`);
683
+ this.context.variables['humanAnswer'] = answer;
684
+ }
685
+ }
686
+ catch (e) {
687
+ console.log(`[Human Help] Ask failed: ${e}`);
688
+ }
689
+ }
690
+ }
691
+ async executeWaitForHuman(step, errors) {
692
+ const conditions = step.blockingConditions || [];
693
+ const checkInterval = step.checkInterval || 2000;
694
+ const resolveTimeout = step.resolveTimeout || 120000;
695
+ const startTime = Date.now();
696
+ while (Date.now() - startTime < resolveTimeout) {
697
+ const stillBlocked = await this.detectBlocking(conditions);
698
+ if (!stillBlocked) {
699
+ console.log('[WaitForHuman] Blocking condition resolved!');
700
+ if (step.onResolved) {
701
+ await this.executeSteps(step.onResolved, errors);
702
+ }
703
+ return;
704
+ }
705
+ await new Promise((r) => setTimeout(r, checkInterval));
706
+ }
707
+ console.log('[WaitForHuman] Timed out waiting for human to resolve.');
708
+ if (step.onTimeout) {
709
+ await this.executeSteps(step.onTimeout, errors);
710
+ }
711
+ else {
712
+ errors.push({
713
+ step: step.id,
714
+ error: `Human intervention timed out after ${resolveTimeout}ms`,
715
+ });
716
+ }
717
+ }
718
+ async executeAutoRecover(step, errors) {
719
+ const conditions = step.blockingConditions || [];
720
+ const isBlocked = await this.detectBlocking(conditions);
721
+ if (!isBlocked) {
722
+ console.log('[AutoRecover] No blocking condition detected, continuing.');
723
+ return;
724
+ }
725
+ console.log('[AutoRecover] Blocking condition detected! Requesting human help...');
726
+ await this.executeHumanHelp(step);
727
+ await this.executeWaitForHuman(step, errors);
728
+ }
729
+ async executeCaptureScript(step) {
730
+ let script;
731
+ if (step.preset) {
732
+ const presetScript = getPreset(step.preset);
733
+ if (presetScript) {
734
+ script = presetScript;
735
+ if (step.captureFilter) {
736
+ script = script.replace(/__FILTER__/g, step.captureFilter);
737
+ }
738
+ else {
739
+ script = script.replace(/__FILTER__/g, '');
740
+ }
741
+ }
742
+ else {
743
+ console.warn(`[captureScript] Unknown preset: ${step.preset}`);
744
+ return;
745
+ }
746
+ }
747
+ else if (step.file) {
748
+ script = readFileSync(step.file, 'utf-8');
749
+ }
750
+ else if (step.value) {
751
+ script = step.value;
752
+ }
753
+ else {
754
+ const outputVar = step.outputVar || 'capturedApiData';
755
+ script = this.generateCaptureScript(outputVar, step.captureFilter);
756
+ }
757
+ const result = await executeCommand(parseCliArgs(['addinitscript', script]), this.browser);
758
+ if (isSuccessResponse(result) && result.tips?.length) {
759
+ console.log('[captureScript] Tips:', result.tips);
760
+ }
761
+ }
762
+ generateCaptureScript(outputVar, filter) {
763
+ const filterStr = filter || '';
764
+ return `
765
+ (function() {
766
+ if (window.__flowCaptureActive) return;
767
+ window.__flowCaptureActive = true;
768
+ var _captured = [];
769
+ var _filter = '${filterStr}';
770
+ var origFetch = window.fetch;
771
+ window.fetch = function() {
772
+ var args = Array.prototype.slice.call(arguments);
773
+ var url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
774
+ return origFetch.apply(this, args).then(function(resp) {
775
+ var ct = (resp.headers.get('content-type') || '').toLowerCase();
776
+ if (!_filter || url.indexOf(_filter) !== -1) {
777
+ if (ct.indexOf('json') !== -1) {
778
+ resp.clone().text().then(function(body) {
779
+ try { _captured.push({ type:'fetch', url:url, status:resp.status, body:JSON.parse(body), ts:Date.now() }); } catch(e) { _captured.push({ type:'fetch', url:url, status:resp.status, body:body, ts:Date.now() }); }
780
+ });
781
+ } else if (ct.indexOf('text/event-stream') !== -1) {
782
+ var reader = resp.clone().body.getReader();
783
+ var decoder = new TextDecoder();
784
+ (function pump() {
785
+ reader.read().then(function(result) {
786
+ if (result.done) return;
787
+ var text = decoder.decode(result.value, {stream:true});
788
+ _captured.push({ type:'sse', url:url, data:text, ts:Date.now() });
789
+ pump();
790
+ });
791
+ })();
792
+ }
793
+ }
794
+ return resp;
795
+ });
796
+ };
797
+ var origOpen = XMLHttpRequest.prototype.open;
798
+ var origSend = XMLHttpRequest.prototype.send;
799
+ XMLHttpRequest.prototype.open = function(method, url) {
800
+ this.__captureUrl = url;
801
+ this.__captureMethod = method;
802
+ return origOpen.apply(this, arguments);
803
+ };
804
+ XMLHttpRequest.prototype.send = function() {
805
+ var self = this;
806
+ this.addEventListener('load', function() {
807
+ var url = self.__captureUrl || '';
808
+ if (!_filter || url.indexOf(_filter) !== -1) {
809
+ var body = self.responseText;
810
+ try { body = JSON.parse(body); } catch(e) {}
811
+ _captured.push({ type:'xhr', url:url, method:self.__captureMethod, status:self.status, body:body, ts:Date.now() });
812
+ }
813
+ });
814
+ return origSend.apply(this, arguments);
815
+ };
816
+ var OrigES = window.EventSource;
817
+ if (OrigES) {
818
+ window.EventSource = function(url, config) {
819
+ var es = new OrigES(url, config);
820
+ if (!_filter || url.indexOf(_filter) !== -1) {
821
+ es.addEventListener('message', function(e) {
822
+ _captured.push({ type:'sse-event', url:url, data:e.data, ts:Date.now() });
823
+ });
824
+ }
825
+ return es;
826
+ };
827
+ window.EventSource.prototype = OrigES.prototype;
828
+ window.EventSource.prototype.constructor = window.EventSource;
829
+ if (OrigES.CONNECTING !== undefined) window.EventSource.CONNECTING = OrigES.CONNECTING;
830
+ if (OrigES.OPEN !== undefined) window.EventSource.OPEN = OrigES.OPEN;
831
+ if (OrigES.CLOSED !== undefined) window.EventSource.CLOSED = OrigES.CLOSED;
832
+ }
833
+ window.__getFlowCapture = function() {
834
+ return JSON.parse(JSON.stringify(_captured));
835
+ };
836
+ window.__clearFlowCapture = function() {
837
+ _captured = [];
838
+ };
839
+ window.__flowCaptureCount = function() {
840
+ return _captured.length;
841
+ };
842
+ })();
843
+ `;
844
+ }
845
+ async readCapturedData() {
846
+ const result = await executeCommand(parseCliArgs([
847
+ 'eval',
848
+ 'JSON.stringify(window.__getFlowCapture ? window.__getFlowCapture() : [])',
849
+ ]), this.browser);
850
+ if (isSuccessResponse(result)) {
851
+ const raw = result.data.result;
852
+ try {
853
+ const captured = typeof raw === 'string' ? JSON.parse(raw) : raw;
854
+ return Array.isArray(captured) ? captured : [];
855
+ }
856
+ catch {
857
+ return [];
858
+ }
859
+ }
860
+ return [];
861
+ }
862
+ async executeCaptureAPI(step) {
863
+ const apiUrl = step.apiUrl || '';
864
+ const outputVar = step.outputVar || 'apiData';
865
+ await executeCommand(parseCliArgs(['network', 'requests', '--clear']), this.browser);
866
+ await executeCommand(parseCliArgs(['network', 'requests', '--capture-response']), this.browser);
867
+ this.context.variables['_captureApiUrl'] = apiUrl;
868
+ this.context.variables['_captureApiVar'] = outputVar;
869
+ }
870
+ async executeReadAPI(step) {
871
+ const apiUrl = step.apiUrl || String(this.context.variables['_captureApiUrl'] || '');
872
+ const outputVar = step.outputVar || String(this.context.variables['_captureApiVar'] || 'apiData');
873
+ const args = ['network', 'requests'];
874
+ if (apiUrl)
875
+ args.push('--filter', apiUrl);
876
+ args.push('--type', 'json');
877
+ const result = await executeCommand(parseCliArgs(args), this.browser);
878
+ if (isSuccessResponse(result)) {
879
+ const data = result.data;
880
+ const requests = data.requests || [];
881
+ const apiData = requests
882
+ .filter((r) => r.responseBody)
883
+ .map((r) => ({
884
+ url: r.url,
885
+ method: r.method,
886
+ status: r.status,
887
+ body: r.responseBody,
888
+ }));
889
+ this.context.results[outputVar] = apiData;
890
+ }
891
+ }
892
+ async executeInterceptRoute(step) {
893
+ const url = step.url || '**';
894
+ if (step.abortRequests) {
895
+ await this.browser.addRoute(url, { abort: true });
896
+ }
897
+ else if (step.mockResponse) {
898
+ const mockBody = this.substituteVars(step.mockResponse);
899
+ const mockStatus = step.mockStatus || 200;
900
+ await this.browser.addRoute(url, {
901
+ response: {
902
+ body: mockBody,
903
+ contentType: 'application/json',
904
+ status: mockStatus,
905
+ },
906
+ });
907
+ }
908
+ }
909
+ async executeRemoveRoute(step) {
910
+ if (step.url) {
911
+ await this.browser.removeRoute(step.url);
912
+ }
913
+ else {
914
+ await this.browser.removeRoute('');
915
+ }
916
+ }
917
+ async executeSmartExtract(step, errors) {
918
+ const config = step.smartExtractConfig || {};
919
+ const outputVar = step.outputVar || 'extracted';
920
+ const minResults = config.minResults || 1;
921
+ const container = config.container || 'body';
922
+ const fields = step.fields || {};
923
+ let layer1Data = [];
924
+ try {
925
+ const apiUrl = config.apiUrl || config.apiFilter || '';
926
+ if (apiUrl) {
927
+ const args = ['network', 'requests', '--type', 'json'];
928
+ if (apiUrl)
929
+ args.push('--filter', apiUrl);
930
+ const result = await executeCommand(parseCliArgs(args), this.browser);
931
+ if (isSuccessResponse(result)) {
932
+ const requests = result.data.requests || [];
933
+ layer1Data = requests
934
+ .filter((r) => r.responseBody)
935
+ .map((r) => {
936
+ const body = r.responseBody;
937
+ if (typeof body === 'string') {
938
+ try {
939
+ return JSON.parse(body);
940
+ }
941
+ catch {
942
+ return { raw: body };
943
+ }
944
+ }
945
+ return body;
946
+ });
947
+ }
948
+ }
949
+ }
950
+ catch {
951
+ /* Layer 1 failed, try next */
952
+ }
953
+ if (layer1Data.length >= minResults) {
954
+ console.log(`[smartExtract] Layer 1 (API) succeeded: ${layer1Data.length} items`);
955
+ this.context.results[outputVar] = layer1Data;
956
+ return;
957
+ }
958
+ let layer2Data = [];
959
+ try {
960
+ const captured = await this.readCapturedData();
961
+ if (config.scriptFilter) {
962
+ layer2Data = captured.filter((item) => item.url && item.url.includes(config.scriptFilter));
963
+ }
964
+ else {
965
+ layer2Data = captured;
966
+ }
967
+ layer2Data = layer2Data
968
+ .filter((item) => item.body)
969
+ .map((item) => (typeof item.body === 'string' ? JSON.parse(item.body) : item.body))
970
+ .filter(Boolean);
971
+ }
972
+ catch {
973
+ /* Layer 2 failed, try next */
974
+ }
975
+ if (layer2Data.length >= minResults) {
976
+ console.log(`[smartExtract] Layer 2 (Script) succeeded: ${layer2Data.length} items`);
977
+ this.context.results[outputVar] = layer2Data;
978
+ return;
979
+ }
980
+ console.log(`[smartExtract] Falling back to Layer 3 (DOM)`);
981
+ await this.executeExtract({
982
+ id: step.id,
983
+ action: 'extract',
984
+ container,
985
+ fields,
986
+ outputVar,
987
+ });
988
+ const domData = this.context.results[outputVar];
989
+ if (Array.isArray(domData) && domData.length >= minResults) {
990
+ console.log(`[smartExtract] Layer 3 (DOM) succeeded: ${domData.length} items`);
991
+ }
992
+ else {
993
+ console.log(`[smartExtract] All layers produced insufficient results`);
994
+ errors.push({
995
+ step: step.id,
996
+ error: `Smart extract failed: API=${layer1Data.length}, Script=${layer2Data.length}, DOM=${Array.isArray(domData) ? domData.length : 0} items`,
997
+ });
998
+ }
999
+ }
1000
+ async resolveSelector(step) {
1001
+ const primarySelector = step.selector || '';
1002
+ if (!step.fallbackSelectors?.length && !step.elementIdentity) {
1003
+ return primarySelector;
1004
+ }
1005
+ const frame = step.inFrame
1006
+ ? this.browser.getFrame(step.inFrame)
1007
+ : this.browser.getPage();
1008
+ try {
1009
+ const primary = frame.locator(primarySelector);
1010
+ if ((await primary.count()) > 0) {
1011
+ return primarySelector;
1012
+ }
1013
+ }
1014
+ catch { }
1015
+ if (step.fallbackSelectors && step.fallbackSelectors.length > 0) {
1016
+ for (const fallback of step.fallbackSelectors) {
1017
+ await new Promise((r) => setTimeout(r, 300));
1018
+ try {
1019
+ const loc = frame.locator(fallback);
1020
+ if ((await loc.count()) > 0) {
1021
+ this.healingLog.push({
1022
+ stepId: step.id || '',
1023
+ originalSelector: primarySelector,
1024
+ healedSelector: fallback,
1025
+ strategy: 'fallback',
1026
+ });
1027
+ return fallback;
1028
+ }
1029
+ }
1030
+ catch { }
1031
+ }
1032
+ }
1033
+ const identityResult = await this.healByIdentity(step, frame);
1034
+ if (identityResult)
1035
+ return identityResult;
1036
+ throw new Error(`Element not found: "${primarySelector}" (all healing strategies exhausted)`);
1037
+ }
1038
+ async healByIdentity(step, frame) {
1039
+ const identity = step.elementIdentity;
1040
+ if (!identity)
1041
+ return null;
1042
+ if (identity.textContent && identity.textContent.length > 0) {
1043
+ const text = identity.textContent.slice(0, 30);
1044
+ try {
1045
+ const selector = `${identity.tagName}:text-is("${text}")`;
1046
+ const loc = frame.locator(selector);
1047
+ if ((await loc.count()) === 1) {
1048
+ this.healingLog.push({
1049
+ stepId: step.id || '',
1050
+ originalSelector: step.selector || '',
1051
+ healedSelector: selector,
1052
+ strategy: 'identity_text',
1053
+ });
1054
+ return selector;
1055
+ }
1056
+ }
1057
+ catch { }
1058
+ }
1059
+ if (identity.attributes) {
1060
+ for (const [attr, value] of Object.entries(identity.attributes)) {
1061
+ if (!value)
1062
+ continue;
1063
+ try {
1064
+ const selector = `${identity.tagName}[${attr}="${value}"]`;
1065
+ const loc = frame.locator(selector);
1066
+ if ((await loc.count()) === 1) {
1067
+ this.healingLog.push({
1068
+ stepId: step.id || '',
1069
+ originalSelector: step.selector || '',
1070
+ healedSelector: selector,
1071
+ strategy: 'identity_attr',
1072
+ });
1073
+ return selector;
1074
+ }
1075
+ }
1076
+ catch { }
1077
+ }
1078
+ }
1079
+ if (identity.parentSignature) {
1080
+ try {
1081
+ const parent = frame.locator(identity.parentSignature);
1082
+ if ((await parent.count()) > 0) {
1083
+ const child = parent.locator(identity.tagName).first();
1084
+ if ((await child.count()) > 0) {
1085
+ const selector = `${identity.parentSignature} > ${identity.tagName}`;
1086
+ this.healingLog.push({
1087
+ stepId: step.id || '',
1088
+ originalSelector: step.selector || '',
1089
+ healedSelector: selector,
1090
+ strategy: 'identity_parent',
1091
+ });
1092
+ return selector;
1093
+ }
1094
+ }
1095
+ }
1096
+ catch { }
1097
+ }
1098
+ return null;
1099
+ }
1100
+ async verifyCheckpoint(checkpoint, frame) {
1101
+ const failures = [];
1102
+ if (checkpoint.urlPattern) {
1103
+ const url = frame.url();
1104
+ const pattern = checkpoint.urlPattern;
1105
+ if (!url.startsWith(pattern) && !url.includes(pattern)) {
1106
+ failures.push(`URL mismatch: expected pattern "${pattern}", got "${url}"`);
1107
+ }
1108
+ }
1109
+ if (checkpoint.elementChecks) {
1110
+ for (const check of checkpoint.elementChecks) {
1111
+ try {
1112
+ const loc = frame.locator(check.selector);
1113
+ const count = await loc.count();
1114
+ if (check.exists && count === 0) {
1115
+ failures.push(`Element missing: "${check.selector}" should exist`);
1116
+ }
1117
+ else if (!check.exists && count > 0) {
1118
+ failures.push(`Element unexpected: "${check.selector}" should not exist`);
1119
+ }
1120
+ if (check.visible && count > 0) {
1121
+ const isVisible = await loc.first().isVisible();
1122
+ if (!isVisible) {
1123
+ failures.push(`Element not visible: "${check.selector}"`);
1124
+ }
1125
+ }
1126
+ if (check.textContent && count > 0) {
1127
+ const actual = (await loc.first().textContent()) || '';
1128
+ if (!actual.includes(check.textContent)) {
1129
+ failures.push(`Element text mismatch: "${check.selector}" expected to contain "${check.textContent}", got "${actual.substring(0, 100)}"`);
1130
+ }
1131
+ }
1132
+ }
1133
+ catch (e) {
1134
+ failures.push(`Element check error: "${check.selector}" - ${e.message}`);
1135
+ }
1136
+ }
1137
+ }
1138
+ return { passed: failures.length === 0, failures };
1139
+ }
1140
+ async waitForDOMStable(frame, timeout = 3000) {
1141
+ await frame
1142
+ .waitForFunction(() => new Promise((resolve) => {
1143
+ let timer;
1144
+ const observer = new MutationObserver(() => {
1145
+ clearTimeout(timer);
1146
+ timer = setTimeout(() => {
1147
+ observer.disconnect();
1148
+ resolve(true);
1149
+ }, 200);
1150
+ });
1151
+ observer.observe(document.body, { childList: true, subtree: true });
1152
+ timer = setTimeout(() => {
1153
+ observer.disconnect();
1154
+ resolve(true);
1155
+ }, 200);
1156
+ }), { timeout })
1157
+ .catch(() => { });
1158
+ }
1159
+ getHealingLog() {
1160
+ return this.healingLog;
1161
+ }
1162
+ getBrowser() {
1163
+ return this.browser;
1164
+ }
1165
+ getContext() {
1166
+ return this.context;
1167
+ }
1168
+ }
1169
+ //# sourceMappingURL=flow-executor.js.map