browser-pilot 0.0.7 → 0.0.9

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.
package/README.md CHANGED
@@ -34,6 +34,7 @@ await browser.close();
34
34
  | Single-selector API (fragile) | Multi-selector by default: `['#primary', '.fallback']` |
35
35
  | No action batching (high latency) | Batch DSL: one call for entire sequences |
36
36
  | No AI-optimized snapshots | Built-in accessibility tree extraction |
37
+ | No audio I/O for voice agents | Mic injection + output capture + Whisper transcription |
37
38
 
38
39
  ## Installation
39
40
 
@@ -246,6 +247,26 @@ await page.setLocale('fr-FR');
246
247
 
247
248
  Devices: `iPhone 14`, `iPhone 14 Pro Max`, `Pixel 7`, `iPad Pro 11`, `Desktop Chrome`, `Desktop Firefox`
248
249
 
250
+ ### Audio I/O
251
+
252
+ ```typescript
253
+ // Set up audio input/output interception
254
+ await page.setupAudio();
255
+
256
+ // Play audio into the page's fake microphone
257
+ await page.audioInput.play(wavBytes, { waitForEnd: true });
258
+
259
+ // Capture audio output until silence
260
+ const capture = await page.audioOutput.captureUntilSilence({ silenceTimeout: 5000 });
261
+
262
+ // Full round-trip: play input → capture response
263
+ const result = await page.audioRoundTrip({ input: wavBytes, silenceTimeout: 5000 });
264
+
265
+ // Transcribe captured audio (requires OPENAI_API_KEY)
266
+ import { transcribe } from 'browser-pilot';
267
+ const { text } = await transcribe(capture);
268
+ ```
269
+
249
270
  ### Request Interception
250
271
 
251
272
  ```typescript
@@ -378,7 +399,7 @@ bp exec '[
378
399
  {"action":"fill","selector":"ref:e5","value":"laptop"},
379
400
  {"action":"click","selector":"ref:e12"},
380
401
  {"action":"snapshot"}
381
- ]' --output json
402
+ ]' --format json
382
403
  ```
383
404
 
384
405
  Multi-selector fallbacks for robustness:
@@ -403,6 +424,44 @@ Output:
403
424
 
404
425
  Run `bp actions` for complete action reference.
405
426
 
427
+ ### Voice Agent Testing
428
+
429
+ Test audio-based AI apps (voice assistants, phone agents) by injecting microphone input and capturing spoken responses.
430
+
431
+ > **Full guide:** [Voice Agent Testing Guide](./docs/guides/voice-agent-testing.md)
432
+
433
+ ```bash
434
+ export OPENAI_API_KEY=sk-... # Required for --transcribe
435
+
436
+ # Validate audio pipeline
437
+ bp audio check -s my-session
438
+ # Output: "READY for roundtrip" with agent AudioContext detected
439
+
440
+ # Full round-trip: send audio prompt → wait for response → transcribe
441
+ bp audio roundtrip -i prompt.wav --transcribe --silence-timeout 1500
442
+ # Output: { "transcript": "Welcome! I'd be happy to help...", "latencyMs": 5200, ... }
443
+
444
+ # Save response audio for manual review
445
+ bp audio roundtrip -i prompt.wav -o response.wav --transcribe
446
+ ```
447
+
448
+ **Important:** Audio overrides must be injected before the voice agent initializes. Use `bp audio check` to validate the pipeline. See the [full guide](./docs/guides/voice-agent-testing.md) for setup order and troubleshooting.
449
+
450
+ Programmatic API:
451
+
452
+ ```typescript
453
+ await page.setupAudio();
454
+
455
+ const result = await page.audioRoundTrip({
456
+ input: audioBytes,
457
+ silenceTimeout: 1500,
458
+ });
459
+
460
+ import { transcribe } from 'browser-pilot';
461
+ const { text } = await transcribe(result.audio);
462
+ console.log(text); // "Welcome! I'd be happy to help..."
463
+ ```
464
+
406
465
  ### Recording Browser Actions
407
466
 
408
467
  Record human interactions to create automation recipes:
@@ -573,6 +632,7 @@ See the [docs](./docs) folder for detailed documentation:
573
632
  - [Multi-Selector Guide](./docs/guides/multi-selector.md)
574
633
  - [Batch Actions](./docs/guides/batch-actions.md)
575
634
  - [Snapshots](./docs/guides/snapshots.md)
635
+ - [Voice Agent Testing](./docs/guides/voice-agent-testing.md)
576
636
  - [CLI Reference](./docs/cli.md)
577
637
  - [API Reference](./docs/api/page.md)
578
638
 
package/dist/actions.cjs CHANGED
@@ -21,10 +21,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var actions_exports = {};
22
22
  __export(actions_exports, {
23
23
  BatchExecutor: () => BatchExecutor,
24
- addBatchToPage: () => addBatchToPage
24
+ addBatchToPage: () => addBatchToPage,
25
+ validateSteps: () => validateSteps
25
26
  });
26
27
  module.exports = __toCommonJS(actions_exports);
27
28
 
29
+ // src/browser/types.ts
30
+ var ElementNotFoundError = class extends Error {
31
+ selectors;
32
+ hints;
33
+ constructor(selectors, hints) {
34
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
35
+ super(`Element not found: ${selectorList.join(", ")}`);
36
+ this.name = "ElementNotFoundError";
37
+ this.selectors = selectorList;
38
+ this.hints = hints;
39
+ }
40
+ };
41
+
28
42
  // src/actions/executor.ts
29
43
  var DEFAULT_TIMEOUT = 3e4;
30
44
  var BatchExecutor = class {
@@ -56,13 +70,15 @@ var BatchExecutor = class {
56
70
  });
57
71
  } catch (error) {
58
72
  const errorMessage = error instanceof Error ? error.message : String(error);
73
+ const hints = error instanceof ElementNotFoundError ? error.hints : void 0;
59
74
  results.push({
60
75
  index: i,
61
76
  action: step.action,
62
77
  selector: step.selector,
63
78
  success: false,
64
79
  durationMs: Date.now() - stepStart,
65
- error: errorMessage
80
+ error: errorMessage,
81
+ hints
66
82
  });
67
83
  if (onFail === "stop" && !step.optional) {
68
84
  return {
@@ -251,17 +267,41 @@ var BatchExecutor = class {
251
267
  await this.page.switchToMain();
252
268
  return {};
253
269
  }
254
- default:
255
- throw new Error(
256
- `Unknown action: ${step.action}. Run 'bp actions' for available actions.`
257
- );
270
+ default: {
271
+ const action = step.action;
272
+ const aliases = {
273
+ execute: "evaluate",
274
+ navigate: "goto",
275
+ input: "fill",
276
+ tap: "click",
277
+ go: "goto",
278
+ run: "evaluate",
279
+ capture: "screenshot",
280
+ inspect: "snapshot",
281
+ enter: "press",
282
+ open: "goto",
283
+ visit: "goto",
284
+ eval: "evaluate",
285
+ js: "evaluate",
286
+ snap: "snapshot",
287
+ frame: "switchFrame"
288
+ };
289
+ const suggestion = aliases[action.toLowerCase()];
290
+ const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
291
+ const valid = "goto, click, fill, type, select, check, uncheck, submit, press, focus, hover, scroll, wait, snapshot, screenshot, evaluate, text, switchFrame, switchToMain";
292
+ throw new Error(`Unknown action "${action}".${hint}
293
+
294
+ Valid actions: ${valid}`);
295
+ }
258
296
  }
259
297
  }
260
298
  /**
261
- * Get the first selector if multiple were provided
262
- * (actual used selector tracking would need to be implemented in Page)
299
+ * Get the actual selector that matched the element.
300
+ * Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
263
301
  */
264
302
  getUsedSelector(selector) {
303
+ const matched = this.page.getLastMatchedSelector();
304
+ if (matched) return matched;
265
305
  return Array.isArray(selector) ? selector[0] : selector;
266
306
  }
267
307
  };
@@ -271,8 +311,444 @@ function addBatchToPage(page) {
271
311
  batch: (steps, options) => executor.execute(steps, options)
272
312
  });
273
313
  }
314
+
315
+ // src/actions/validate.ts
316
+ function levenshtein(a, b) {
317
+ const m = a.length;
318
+ const n = b.length;
319
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
320
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
321
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
322
+ for (let i = 1; i <= m; i++) {
323
+ for (let j = 1; j <= n; j++) {
324
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
325
+ }
326
+ }
327
+ return dp[m][n];
328
+ }
329
+ var ACTION_ALIASES = {
330
+ execute: "evaluate",
331
+ navigate: "goto",
332
+ input: "fill",
333
+ tap: "click",
334
+ go: "goto",
335
+ run: "evaluate",
336
+ capture: "screenshot",
337
+ inspect: "snapshot",
338
+ enter: "press",
339
+ keypress: "press",
340
+ nav: "goto",
341
+ open: "goto",
342
+ visit: "goto",
343
+ browse: "goto",
344
+ load: "goto",
345
+ write: "fill",
346
+ set: "fill",
347
+ pick: "select",
348
+ choose: "select",
349
+ send: "press",
350
+ eval: "evaluate",
351
+ js: "evaluate",
352
+ script: "evaluate",
353
+ snap: "snapshot",
354
+ accessibility: "snapshot",
355
+ a11y: "snapshot",
356
+ image: "screenshot",
357
+ pic: "screenshot",
358
+ frame: "switchFrame",
359
+ iframe: "switchFrame"
360
+ };
361
+ var PROPERTY_ALIASES = {
362
+ expression: "value",
363
+ href: "url",
364
+ target: "selector",
365
+ element: "selector",
366
+ code: "value",
367
+ script: "value",
368
+ src: "url",
369
+ link: "url",
370
+ char: "key",
371
+ text: "value",
372
+ query: "selector",
373
+ el: "selector",
374
+ elem: "selector",
375
+ css: "selector",
376
+ xpath: "selector",
377
+ input: "value",
378
+ content: "value",
379
+ keys: "key",
380
+ button: "key",
381
+ address: "url",
382
+ page: "url",
383
+ path: "url"
384
+ };
385
+ var ACTION_RULES = {
386
+ goto: {
387
+ required: { url: { type: "string" } },
388
+ optional: {}
389
+ },
390
+ click: {
391
+ required: { selector: { type: "string|string[]" } },
392
+ optional: {
393
+ waitForNavigation: { type: "boolean" }
394
+ }
395
+ },
396
+ fill: {
397
+ required: { selector: { type: "string|string[]" }, value: { type: "string" } },
398
+ optional: {
399
+ clear: { type: "boolean" },
400
+ blur: { type: "boolean" }
401
+ }
402
+ },
403
+ type: {
404
+ required: { selector: { type: "string|string[]" }, value: { type: "string" } },
405
+ optional: {
406
+ delay: { type: "number" }
407
+ }
408
+ },
409
+ select: {
410
+ required: {},
411
+ optional: {
412
+ selector: { type: "string|string[]" },
413
+ value: { type: "string|string[]" },
414
+ trigger: { type: "string|string[]" },
415
+ option: { type: "string|string[]" },
416
+ match: { type: "string", enum: ["text", "value", "contains"] }
417
+ }
418
+ },
419
+ check: {
420
+ required: { selector: { type: "string|string[]" } },
421
+ optional: {}
422
+ },
423
+ uncheck: {
424
+ required: { selector: { type: "string|string[]" } },
425
+ optional: {}
426
+ },
427
+ submit: {
428
+ required: { selector: { type: "string|string[]" } },
429
+ optional: {
430
+ method: { type: "string", enum: ["enter", "click", "enter+click"] }
431
+ }
432
+ },
433
+ press: {
434
+ required: { key: { type: "string" } },
435
+ optional: {}
436
+ },
437
+ focus: {
438
+ required: { selector: { type: "string|string[]" } },
439
+ optional: {}
440
+ },
441
+ hover: {
442
+ required: { selector: { type: "string|string[]" } },
443
+ optional: {}
444
+ },
445
+ scroll: {
446
+ required: {},
447
+ optional: {
448
+ selector: { type: "string|string[]" },
449
+ x: { type: "number" },
450
+ y: { type: "number" },
451
+ direction: { type: "string", enum: ["up", "down", "left", "right"] },
452
+ amount: { type: "number" }
453
+ }
454
+ },
455
+ wait: {
456
+ required: {},
457
+ optional: {
458
+ selector: { type: "string|string[]" },
459
+ waitFor: {
460
+ type: "string",
461
+ enum: ["visible", "hidden", "attached", "detached", "navigation", "networkIdle"]
462
+ }
463
+ }
464
+ },
465
+ snapshot: {
466
+ required: {},
467
+ optional: {}
468
+ },
469
+ screenshot: {
470
+ required: {},
471
+ optional: {
472
+ format: { type: "string", enum: ["png", "jpeg", "webp"] },
473
+ quality: { type: "number" },
474
+ fullPage: { type: "boolean" }
475
+ }
476
+ },
477
+ evaluate: {
478
+ required: { value: { type: "string" } },
479
+ optional: {}
480
+ },
481
+ text: {
482
+ required: {},
483
+ optional: {
484
+ selector: { type: "string|string[]" }
485
+ }
486
+ },
487
+ switchFrame: {
488
+ required: { selector: { type: "string|string[]" } },
489
+ optional: {}
490
+ },
491
+ switchToMain: {
492
+ required: {},
493
+ optional: {}
494
+ }
495
+ };
496
+ var VALID_ACTIONS = Object.keys(ACTION_RULES);
497
+ var VALID_ACTIONS_LIST = VALID_ACTIONS.join(", ");
498
+ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
499
+ "action",
500
+ "selector",
501
+ "url",
502
+ "value",
503
+ "key",
504
+ "waitFor",
505
+ "timeout",
506
+ "optional",
507
+ "method",
508
+ "clear",
509
+ "blur",
510
+ "delay",
511
+ "waitForNavigation",
512
+ "trigger",
513
+ "option",
514
+ "match",
515
+ "x",
516
+ "y",
517
+ "direction",
518
+ "amount",
519
+ "format",
520
+ "quality",
521
+ "fullPage"
522
+ ]);
523
+ function resolveAction(name) {
524
+ if (VALID_ACTIONS.includes(name)) {
525
+ return { action: name };
526
+ }
527
+ const lower = name.toLowerCase();
528
+ if (ACTION_ALIASES[lower]) {
529
+ return {
530
+ action: ACTION_ALIASES[lower],
531
+ suggestion: `Did you mean "${ACTION_ALIASES[lower]}"?`
532
+ };
533
+ }
534
+ let best = null;
535
+ let bestDist = Infinity;
536
+ for (const valid of VALID_ACTIONS) {
537
+ const dist = levenshtein(lower, valid);
538
+ if (dist < bestDist) {
539
+ bestDist = dist;
540
+ best = valid;
541
+ }
542
+ }
543
+ if (best && bestDist <= 2) {
544
+ return { action: best, suggestion: `Did you mean "${best}"?` };
545
+ }
546
+ return null;
547
+ }
548
+ function suggestProperty(name) {
549
+ if (PROPERTY_ALIASES[name]) {
550
+ return PROPERTY_ALIASES[name];
551
+ }
552
+ let best = null;
553
+ let bestDist = Infinity;
554
+ for (const known of KNOWN_STEP_FIELDS) {
555
+ if (known === "action") continue;
556
+ const dist = levenshtein(name, known);
557
+ if (dist < bestDist) {
558
+ bestDist = dist;
559
+ best = known;
560
+ }
561
+ }
562
+ if (best && bestDist <= 2) {
563
+ return best;
564
+ }
565
+ return void 0;
566
+ }
567
+ function checkFieldType(value, rule) {
568
+ switch (rule.type) {
569
+ case "string":
570
+ if (typeof value !== "string") return `expected string, got ${typeof value}`;
571
+ if (rule.enum && !rule.enum.includes(value)) {
572
+ return `must be one of: ${rule.enum.join(", ")}`;
573
+ }
574
+ return null;
575
+ case "string|string[]":
576
+ if (typeof value !== "string" && !Array.isArray(value)) {
577
+ return `expected string or string[], got ${typeof value}`;
578
+ }
579
+ if (Array.isArray(value) && value.some((v) => typeof v !== "string")) {
580
+ return "array elements must be strings";
581
+ }
582
+ return null;
583
+ case "number":
584
+ if (typeof value !== "number") return `expected number, got ${typeof value}`;
585
+ return null;
586
+ case "boolean":
587
+ if (typeof value !== "boolean") return `expected boolean, got ${typeof value}`;
588
+ return null;
589
+ }
590
+ }
591
+ function validateSteps(steps) {
592
+ const errors = [];
593
+ for (let i = 0; i < steps.length; i++) {
594
+ const step = steps[i];
595
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
596
+ errors.push({
597
+ stepIndex: i,
598
+ field: "step",
599
+ message: "step must be a JSON object."
600
+ });
601
+ continue;
602
+ }
603
+ const obj = step;
604
+ if (!("action" in obj)) {
605
+ errors.push({
606
+ stepIndex: i,
607
+ field: "action",
608
+ message: 'missing required "action" field.'
609
+ });
610
+ continue;
611
+ }
612
+ const actionName = obj["action"];
613
+ if (typeof actionName !== "string") {
614
+ errors.push({
615
+ stepIndex: i,
616
+ field: "action",
617
+ message: `"action" must be a string, got ${typeof actionName}.`
618
+ });
619
+ continue;
620
+ }
621
+ const resolved = resolveAction(actionName);
622
+ if (!resolved) {
623
+ errors.push({
624
+ stepIndex: i,
625
+ field: "action",
626
+ message: `unknown action "${actionName}".`,
627
+ suggestion: `Valid actions: ${VALID_ACTIONS_LIST}`
628
+ });
629
+ continue;
630
+ }
631
+ if (resolved.suggestion) {
632
+ errors.push({
633
+ stepIndex: i,
634
+ field: "action",
635
+ message: `unknown action "${actionName}". ${resolved.suggestion}`,
636
+ suggestion: resolved.suggestion
637
+ });
638
+ continue;
639
+ }
640
+ const action = resolved.action;
641
+ const rule = ACTION_RULES[action];
642
+ for (const key of Object.keys(obj)) {
643
+ if (key === "action") continue;
644
+ if (!KNOWN_STEP_FIELDS.has(key)) {
645
+ const suggestion = suggestProperty(key);
646
+ errors.push({
647
+ stepIndex: i,
648
+ field: key,
649
+ message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
650
+ suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
651
+ });
652
+ }
653
+ }
654
+ for (const [field, fieldRule] of Object.entries(rule.required)) {
655
+ if (!(field in obj) || obj[field] === void 0) {
656
+ errors.push({
657
+ stepIndex: i,
658
+ field,
659
+ message: `missing required "${field}" (${fieldRule.type}).`
660
+ });
661
+ } else {
662
+ const typeErr = checkFieldType(obj[field], fieldRule);
663
+ if (typeErr) {
664
+ errors.push({
665
+ stepIndex: i,
666
+ field,
667
+ message: `"${field}" ${typeErr}.`
668
+ });
669
+ }
670
+ }
671
+ }
672
+ for (const [field, fieldRule] of Object.entries(rule.optional)) {
673
+ if (field in obj && obj[field] !== void 0) {
674
+ const typeErr = checkFieldType(obj[field], fieldRule);
675
+ if (typeErr) {
676
+ errors.push({
677
+ stepIndex: i,
678
+ field,
679
+ message: `"${field}" ${typeErr}.`
680
+ });
681
+ }
682
+ }
683
+ }
684
+ if ("timeout" in obj && obj["timeout"] !== void 0) {
685
+ if (typeof obj["timeout"] !== "number") {
686
+ errors.push({
687
+ stepIndex: i,
688
+ field: "timeout",
689
+ message: `"timeout" expected number, got ${typeof obj["timeout"]}.`
690
+ });
691
+ }
692
+ }
693
+ if ("optional" in obj && obj["optional"] !== void 0) {
694
+ if (typeof obj["optional"] !== "boolean") {
695
+ errors.push({
696
+ stepIndex: i,
697
+ field: "optional",
698
+ message: `"optional" expected boolean, got ${typeof obj["optional"]}.`
699
+ });
700
+ }
701
+ }
702
+ if (action === "select") {
703
+ const hasNative = "selector" in obj && "value" in obj;
704
+ const hasCustom = "trigger" in obj && "option" in obj && "value" in obj;
705
+ if (!hasNative && !hasCustom) {
706
+ errors.push({
707
+ stepIndex: i,
708
+ field: "selector",
709
+ message: "select requires either (selector + value) for native select, or (trigger + option + value) for custom select."
710
+ });
711
+ }
712
+ }
713
+ }
714
+ return {
715
+ valid: errors.length === 0,
716
+ errors,
717
+ formatted() {
718
+ if (errors.length === 0) return "";
719
+ const lines = [`Validation failed (${errors.length} error${errors.length > 1 ? "s" : ""}):`];
720
+ for (const err of errors) {
721
+ const stepLabel = err.field === "action" || err.field === "step" ? `Step ${err.stepIndex}` : `Step ${err.stepIndex}`;
722
+ lines.push("");
723
+ lines.push(` ${stepLabel}: ${err.message}`);
724
+ if (err.suggestion && !err.message.includes(err.suggestion)) {
725
+ lines.push(` ${err.suggestion}`);
726
+ }
727
+ const step = steps[err.stepIndex];
728
+ if (step && typeof step === "object") {
729
+ lines.push(` Got: ${JSON.stringify(step)}`);
730
+ }
731
+ }
732
+ const hasEvaluateError = errors.some((err) => {
733
+ const step = steps[err.stepIndex];
734
+ return step && typeof step === "object" && step["action"] === "evaluate";
735
+ });
736
+ if (hasEvaluateError) {
737
+ lines.push("");
738
+ lines.push(
739
+ "Tip: For JavaScript evaluation, use 'bp eval' instead \u2014 no JSON wrapping needed:"
740
+ );
741
+ lines.push(" bp eval 'your.expression.here'");
742
+ }
743
+ lines.push("");
744
+ lines.push(`Valid actions: ${VALID_ACTIONS_LIST}`);
745
+ return lines.join("\n");
746
+ }
747
+ };
748
+ }
274
749
  // Annotate the CommonJS export names for ESM import in node:
275
750
  0 && (module.exports = {
276
751
  BatchExecutor,
277
- addBatchToPage
752
+ addBatchToPage,
753
+ validateSteps
278
754
  });
@@ -1,5 +1,5 @@
1
- import { P as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-TVlTA7nH.cjs';
2
- export { A as ActionType, c as StepResult } from './types-TVlTA7nH.cjs';
1
+ import { y as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-CYw-7vx1.cjs';
2
+ export { A as ActionType, c as StepResult } from './types-CYw-7vx1.cjs';
3
3
  import './client-7Nqka5MV.cjs';
4
4
 
5
5
  /**
@@ -18,8 +18,8 @@ declare class BatchExecutor {
18
18
  */
19
19
  private executeStep;
20
20
  /**
21
- * Get the first selector if multiple were provided
22
- * (actual used selector tracking would need to be implemented in Page)
21
+ * Get the actual selector that matched the element.
22
+ * Uses the last matched selector tracked by Page, falls back to first selector if unavailable.
23
23
  */
24
24
  private getUsedSelector;
25
25
  }
@@ -30,4 +30,23 @@ declare function addBatchToPage(page: Page): Page & {
30
30
  batch: (steps: Step[], options?: BatchOptions) => Promise<BatchResult>;
31
31
  };
32
32
 
33
- export { BatchExecutor, BatchOptions, BatchResult, Step, addBatchToPage };
33
+ /**
34
+ * Step validation for batch executor
35
+ *
36
+ * Validates steps before browser connection, catching malformed JSON
37
+ * from AI agents with actionable, specific feedback.
38
+ */
39
+ interface ValidationError {
40
+ stepIndex: number;
41
+ field: string;
42
+ message: string;
43
+ suggestion?: string;
44
+ }
45
+ interface ValidationResult {
46
+ valid: boolean;
47
+ errors: ValidationError[];
48
+ formatted(): string;
49
+ }
50
+ declare function validateSteps(steps: unknown[]): ValidationResult;
51
+
52
+ export { BatchExecutor, BatchOptions, BatchResult, Step, type ValidationError, type ValidationResult, addBatchToPage, validateSteps };