agentmb 0.1.0 → 0.1.1

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 (36) hide show
  1. package/README.md +82 -1
  2. package/dist/browser/actions.d.ts +272 -0
  3. package/dist/browser/actions.d.ts.map +1 -1
  4. package/dist/browser/actions.js +797 -0
  5. package/dist/browser/actions.js.map +1 -1
  6. package/dist/browser/manager.d.ts +88 -0
  7. package/dist/browser/manager.d.ts.map +1 -1
  8. package/dist/browser/manager.js +231 -0
  9. package/dist/browser/manager.js.map +1 -1
  10. package/dist/cli/client.d.ts +1 -0
  11. package/dist/cli/client.d.ts.map +1 -1
  12. package/dist/cli/client.js +21 -0
  13. package/dist/cli/client.js.map +1 -1
  14. package/dist/cli/commands/actions.d.ts.map +1 -1
  15. package/dist/cli/commands/actions.js +762 -10
  16. package/dist/cli/commands/actions.js.map +1 -1
  17. package/dist/cli/index.js +1 -1
  18. package/dist/daemon/routes/actions.d.ts.map +1 -1
  19. package/dist/daemon/routes/actions.js +529 -6
  20. package/dist/daemon/routes/actions.js.map +1 -1
  21. package/dist/daemon/routes/browser_control.d.ts +12 -0
  22. package/dist/daemon/routes/browser_control.d.ts.map +1 -0
  23. package/dist/daemon/routes/browser_control.js +172 -0
  24. package/dist/daemon/routes/browser_control.js.map +1 -0
  25. package/dist/daemon/routes/interaction.d.ts +11 -0
  26. package/dist/daemon/routes/interaction.d.ts.map +1 -0
  27. package/dist/daemon/routes/interaction.js +176 -0
  28. package/dist/daemon/routes/interaction.js.map +1 -0
  29. package/dist/daemon/routes/state.d.ts +11 -0
  30. package/dist/daemon/routes/state.d.ts.map +1 -0
  31. package/dist/daemon/routes/state.js +190 -0
  32. package/dist/daemon/routes/state.js.map +1 -0
  33. package/dist/daemon/server.d.ts.map +1 -1
  34. package/dist/daemon/server.js +7 -1
  35. package/dist/daemon/server.js.map +1 -1
  36. package/package.json +1 -1
@@ -8,6 +8,9 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const readline_1 = __importDefault(require("readline"));
10
10
  const client_1 = require("../client");
11
+ function collectValues(val, prev) {
12
+ return prev.concat([val]);
13
+ }
11
14
  function printDiagnostics(res) {
12
15
  console.error('Error:', res.error);
13
16
  if (res.url)
@@ -87,26 +90,34 @@ function actionCommands(program) {
87
90
  }
88
91
  });
89
92
  program
90
- .command('click <session-id> <selector>')
91
- .description('Click an element')
92
- .action(async (sessionId, selector) => {
93
- const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/click`, { selector });
93
+ .command('click <session-id> <selector-or-eid>')
94
+ .description('Click an element (use --element-id to treat arg as element_id from element-map)')
95
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
96
+ .action(async (sessionId, selectorOrEid, opts) => {
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ const body = opts.elementId ? { element_id: selectorOrEid } : { selector: selectorOrEid };
99
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/click`, body);
94
100
  if (res.error) {
95
101
  console.error('Error:', res.error);
96
102
  process.exit(1);
97
103
  }
98
- console.log(`✓ Clicked "${selector}" (${res.duration_ms}ms)`);
104
+ console.log(`✓ Clicked "${selectorOrEid}" (${res.duration_ms}ms)`);
99
105
  });
100
106
  program
101
- .command('fill <session-id> <selector> <value>')
102
- .description('Fill a form field')
103
- .action(async (sessionId, selector, value) => {
104
- const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/fill`, { selector, value });
107
+ .command('fill <session-id> <selector-or-eid> <value>')
108
+ .description('Fill a form field (use --element-id to treat first arg as element_id from element-map)')
109
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
110
+ .action(async (sessionId, selectorOrEid, value, opts) => {
111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
+ const body = opts.elementId
113
+ ? { element_id: selectorOrEid, value }
114
+ : { selector: selectorOrEid, value };
115
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/fill`, body);
105
116
  if (res.error) {
106
117
  console.error('Error:', res.error);
107
118
  process.exit(1);
108
119
  }
109
- console.log(`✓ Filled "${selector}" (${res.duration_ms}ms)`);
120
+ console.log(`✓ Filled "${selectorOrEid}" (${res.duration_ms}ms)`);
110
121
  });
111
122
  program
112
123
  .command('logs <session-id>')
@@ -345,5 +356,746 @@ function actionCommands(program) {
345
356
  console.log('Note: CDP WS URL is only available when using a full browser launch (not persistent context).');
346
357
  }
347
358
  });
359
+ // ---------------------------------------------------------------------------
360
+ // R07-T01/T02/T07 — element_map, get, assert, wait-stable
361
+ // ---------------------------------------------------------------------------
362
+ program
363
+ .command('element-map <session-id>')
364
+ .description('Scan the page and return a numbered element map (assigns stable element IDs)')
365
+ .option('--scope <selector>', 'Limit scan to elements inside this CSS selector')
366
+ .option('--limit <n>', 'Max elements to return', '500')
367
+ .option('--json', 'Output raw JSON instead of a table')
368
+ .action(async (sessionId, opts) => {
369
+ const body = { limit: parseInt(opts.limit) };
370
+ if (opts.scope)
371
+ body.scope = opts.scope;
372
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/element_map`, body);
373
+ if (res.error) {
374
+ console.error('Error:', res.error);
375
+ process.exit(1);
376
+ }
377
+ if (opts.json) {
378
+ console.log(JSON.stringify(res, null, 2));
379
+ return;
380
+ }
381
+ const elements = res.elements ?? [];
382
+ if (elements.length === 0) {
383
+ console.log('No interactive elements found.');
384
+ return;
385
+ }
386
+ console.log(`Found ${elements.length} element(s) on ${res.url}:`);
387
+ for (const el of elements) {
388
+ const blocked = el.overlay_blocked ? ' [overlay-blocked]' : '';
389
+ const text = String(el.text ?? '').slice(0, 60).replace(/\n/g, ' ');
390
+ console.log(` ${el.element_id} <${el.tag}> role=${el.role}${blocked} ${text}`);
391
+ }
392
+ });
393
+ program
394
+ .command('get <session-id> <property> <selector-or-eid>')
395
+ .description('Read a property from an element (text|html|value|attr|count|box)')
396
+ .option('--attr-name <name>', 'Attribute name (required when property=attr)')
397
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
398
+ .action(async (sessionId, property, target, opts) => {
399
+ const body = { property };
400
+ if (opts.elementId) {
401
+ body.element_id = target;
402
+ }
403
+ else {
404
+ body.selector = target;
405
+ }
406
+ if (opts.attrName)
407
+ body.attr_name = opts.attrName;
408
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/get`, body);
409
+ if (res.error) {
410
+ console.error('Error:', res.error);
411
+ process.exit(1);
412
+ }
413
+ console.log(JSON.stringify(res.value, null, 2));
414
+ });
415
+ program
416
+ .command('assert <session-id> <property> <selector-or-eid>')
417
+ .description('Assert element state: visible|enabled|checked')
418
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
419
+ .option('--expected <bool>', 'Expected value (true|false)', 'true')
420
+ .action(async (sessionId, property, target, opts) => {
421
+ const body = { property, expected: opts.expected !== 'false' };
422
+ if (opts.elementId) {
423
+ body.element_id = target;
424
+ }
425
+ else {
426
+ body.selector = target;
427
+ }
428
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/assert`, body);
429
+ if (res.error) {
430
+ console.error('Error:', res.error);
431
+ process.exit(1);
432
+ }
433
+ const icon = res.passed ? '✓' : '✗';
434
+ console.log(`${icon} ${property}: actual=${res.actual} expected=${res.expected} — ${res.passed ? 'PASS' : 'FAIL'}`);
435
+ if (!res.passed)
436
+ process.exit(1);
437
+ });
438
+ program
439
+ .command('wait-stable <session-id>')
440
+ .description('Wait for page to be stable (network idle + DOM quiescence)')
441
+ .option('--timeout-ms <ms>', 'Timeout in ms', '10000')
442
+ .option('--dom-stable-ms <ms>', 'DOM must be mutation-free for this many ms', '300')
443
+ .option('--overlay-selector <selector>', 'Also wait until no element matches this selector')
444
+ .action(async (sessionId, opts) => {
445
+ const body = {
446
+ timeout_ms: parseInt(opts.timeoutMs),
447
+ dom_stable_ms: parseInt(opts.domStableMs),
448
+ };
449
+ if (opts.overlaySelector)
450
+ body.overlay_selector = opts.overlaySelector;
451
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/wait_page_stable`, body);
452
+ if (res.error) {
453
+ console.error('Error:', res.error);
454
+ process.exit(1);
455
+ }
456
+ console.log(`✓ Page stable (${res.waited_ms}ms)`);
457
+ });
458
+ // ---------------------------------------------------------------------------
459
+ // R07-T02/T13 — snapshot-map
460
+ // ---------------------------------------------------------------------------
461
+ program
462
+ .command('snapshot-map <session-id>')
463
+ .description('Snapshot the page element map with page_rev tracking (returns ref_ids for stable targeting)')
464
+ .option('--scope <selector>', 'Limit scan to elements inside this CSS selector')
465
+ .option('--limit <n>', 'Max elements to return', '500')
466
+ .option('--json', 'Output raw JSON instead of a table')
467
+ .action(async (sessionId, opts) => {
468
+ const body = { limit: parseInt(opts.limit) };
469
+ if (opts.scope)
470
+ body.scope = opts.scope;
471
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/snapshot_map`, body);
472
+ if (res.error) {
473
+ console.error('Error:', res.error);
474
+ process.exit(1);
475
+ }
476
+ if (opts.json) {
477
+ console.log(JSON.stringify(res, null, 2));
478
+ return;
479
+ }
480
+ const elements = res.elements ?? [];
481
+ console.log(`Snapshot ${res.snapshot_id} (page_rev=${res.page_rev}) — ${elements.length} element(s) on ${res.url}:`);
482
+ for (const el of elements) {
483
+ const blocked = el.overlay_blocked ? ' [overlay-blocked]' : '';
484
+ const text = String(el.text ?? '').slice(0, 60).replace(/\n/g, ' ');
485
+ console.log(` ${el.ref_id} <${el.tag}> role=${el.role}${blocked} ${text}`);
486
+ }
487
+ });
488
+ // ---------------------------------------------------------------------------
489
+ // R07-T03 — additional interaction primitives
490
+ // ---------------------------------------------------------------------------
491
+ program
492
+ .command('dblclick <session-id> <selector-or-eid>')
493
+ .description('Double-click an element')
494
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
495
+ .option('--timeout-ms <ms>', 'Timeout in ms', '5000')
496
+ .action(async (sessionId, selectorOrEid, opts) => {
497
+ const body = opts.elementId
498
+ ? { element_id: selectorOrEid }
499
+ : { selector: selectorOrEid };
500
+ body.timeout_ms = parseInt(opts.timeoutMs);
501
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/dblclick`, body);
502
+ if (res.error) {
503
+ console.error('Error:', res.error);
504
+ process.exit(1);
505
+ }
506
+ console.log(`✓ Double-clicked "${selectorOrEid}" (${res.duration_ms}ms)`);
507
+ });
508
+ program
509
+ .command('focus <session-id> <selector-or-eid>')
510
+ .description('Focus an element')
511
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
512
+ .action(async (sessionId, selectorOrEid, opts) => {
513
+ const body = opts.elementId
514
+ ? { element_id: selectorOrEid }
515
+ : { selector: selectorOrEid };
516
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/focus`, body);
517
+ if (res.error) {
518
+ console.error('Error:', res.error);
519
+ process.exit(1);
520
+ }
521
+ console.log(`✓ Focused "${selectorOrEid}" (${res.duration_ms}ms)`);
522
+ });
523
+ program
524
+ .command('check <session-id> <selector-or-eid>')
525
+ .description('Check a checkbox or radio button')
526
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
527
+ .action(async (sessionId, selectorOrEid, opts) => {
528
+ const body = opts.elementId
529
+ ? { element_id: selectorOrEid }
530
+ : { selector: selectorOrEid };
531
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/check`, body);
532
+ if (res.error) {
533
+ console.error('Error:', res.error);
534
+ process.exit(1);
535
+ }
536
+ console.log(`✓ Checked "${selectorOrEid}" (${res.duration_ms}ms)`);
537
+ });
538
+ program
539
+ .command('uncheck <session-id> <selector-or-eid>')
540
+ .description('Uncheck a checkbox')
541
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
542
+ .action(async (sessionId, selectorOrEid, opts) => {
543
+ const body = opts.elementId
544
+ ? { element_id: selectorOrEid }
545
+ : { selector: selectorOrEid };
546
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/uncheck`, body);
547
+ if (res.error) {
548
+ console.error('Error:', res.error);
549
+ process.exit(1);
550
+ }
551
+ console.log(`✓ Unchecked "${selectorOrEid}" (${res.duration_ms}ms)`);
552
+ });
553
+ program
554
+ .command('scroll <session-id> <selector-or-eid>')
555
+ .description('Scroll an element by delta pixels')
556
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
557
+ .option('--dx <px>', 'Horizontal scroll delta', '0')
558
+ .option('--dy <px>', 'Vertical scroll delta', '300')
559
+ .action(async (sessionId, selectorOrEid, opts) => {
560
+ const body = opts.elementId
561
+ ? { element_id: selectorOrEid }
562
+ : { selector: selectorOrEid };
563
+ body.delta_x = parseInt(opts.dx);
564
+ body.delta_y = parseInt(opts.dy);
565
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/scroll`, body);
566
+ if (res.error) {
567
+ console.error('Error:', res.error);
568
+ process.exit(1);
569
+ }
570
+ console.log(`✓ Scrolled "${selectorOrEid}" (${res.duration_ms}ms)`);
571
+ });
572
+ program
573
+ .command('scroll-into-view <session-id> <selector-or-eid>')
574
+ .description('Scroll element into view')
575
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
576
+ .action(async (sessionId, selectorOrEid, opts) => {
577
+ const body = opts.elementId
578
+ ? { element_id: selectorOrEid }
579
+ : { selector: selectorOrEid };
580
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/scroll_into_view`, body);
581
+ if (res.error) {
582
+ console.error('Error:', res.error);
583
+ process.exit(1);
584
+ }
585
+ console.log(`✓ Scrolled "${selectorOrEid}" into view (${res.duration_ms}ms)`);
586
+ });
587
+ program
588
+ .command('drag <session-id> <source> <target>')
589
+ .description('Drag an element from source to target (CSS selectors)')
590
+ .option('--timeout-ms <ms>', 'Timeout in ms', '5000')
591
+ .action(async (sessionId, source, target, opts) => {
592
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/drag`, { source, target, timeout_ms: parseInt(opts.timeoutMs) });
593
+ if (res.error) {
594
+ console.error('Error:', res.error);
595
+ process.exit(1);
596
+ }
597
+ console.log(`✓ Dragged "${source}" → "${target}" (${res.duration_ms}ms)`);
598
+ });
599
+ program
600
+ .command('mouse-move <session-id> <x> <y>')
601
+ .description('Move mouse to absolute page coordinates')
602
+ .action(async (sessionId, x, y) => {
603
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/mouse_move`, { x: parseFloat(x), y: parseFloat(y) });
604
+ if (res.error) {
605
+ console.error('Error:', res.error);
606
+ process.exit(1);
607
+ }
608
+ console.log(`✓ Mouse moved to (${x},${y}) (${res.duration_ms}ms)`);
609
+ });
610
+ program
611
+ .command('mouse-down <session-id>')
612
+ .description('Press the left mouse button at current position')
613
+ .option('--button <btn>', 'Mouse button: left|right|middle', 'left')
614
+ .action(async (sessionId, opts) => {
615
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/mouse_down`, { button: opts.button });
616
+ if (res.error) {
617
+ console.error('Error:', res.error);
618
+ process.exit(1);
619
+ }
620
+ console.log(`✓ Mouse down (${res.duration_ms}ms)`);
621
+ });
622
+ program
623
+ .command('mouse-up <session-id>')
624
+ .description('Release the left mouse button at current position')
625
+ .option('--button <btn>', 'Mouse button: left|right|middle', 'left')
626
+ .action(async (sessionId, opts) => {
627
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/mouse_up`, { button: opts.button });
628
+ if (res.error) {
629
+ console.error('Error:', res.error);
630
+ process.exit(1);
631
+ }
632
+ console.log(`✓ Mouse up (${res.duration_ms}ms)`);
633
+ });
634
+ program
635
+ .command('key-down <session-id> <key>')
636
+ .description('Press a keyboard key (hold down)')
637
+ .action(async (sessionId, key) => {
638
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/key_down`, { key });
639
+ if (res.error) {
640
+ console.error('Error:', res.error);
641
+ process.exit(1);
642
+ }
643
+ console.log(`✓ Key down "${key}" (${res.duration_ms}ms)`);
644
+ });
645
+ program
646
+ .command('key-up <session-id> <key>')
647
+ .description('Release a keyboard key')
648
+ .action(async (sessionId, key) => {
649
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/key_up`, { key });
650
+ if (res.error) {
651
+ console.error('Error:', res.error);
652
+ process.exit(1);
653
+ }
654
+ console.log(`✓ Key up "${key}" (${res.duration_ms}ms)`);
655
+ });
656
+ // ---------------------------------------------------------------------------
657
+ // R07-T04 — navigation control
658
+ // ---------------------------------------------------------------------------
659
+ program
660
+ .command('back <session-id>')
661
+ .description('Navigate back in browser history')
662
+ .option('--timeout-ms <ms>', 'Timeout in ms', '5000')
663
+ .option('--wait-until <event>', 'Wait until event (load|networkidle|commit)', 'load')
664
+ .action(async (sessionId, opts) => {
665
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/back`, { timeout_ms: parseInt(opts.timeoutMs), wait_until: opts.waitUntil });
666
+ if (res.error) {
667
+ console.error('Error:', res.error);
668
+ process.exit(1);
669
+ }
670
+ console.log(`✓ Back → ${res.url} (${res.duration_ms}ms)`);
671
+ });
672
+ program
673
+ .command('forward <session-id>')
674
+ .description('Navigate forward in browser history')
675
+ .option('--timeout-ms <ms>', 'Timeout in ms', '5000')
676
+ .option('--wait-until <event>', 'Wait until event (load|networkidle|commit)', 'load')
677
+ .action(async (sessionId, opts) => {
678
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/forward`, { timeout_ms: parseInt(opts.timeoutMs), wait_until: opts.waitUntil });
679
+ if (res.error) {
680
+ console.error('Error:', res.error);
681
+ process.exit(1);
682
+ }
683
+ console.log(`✓ Forward → ${res.url} (${res.duration_ms}ms)`);
684
+ });
685
+ program
686
+ .command('reload <session-id>')
687
+ .description('Reload the current page')
688
+ .option('--timeout-ms <ms>', 'Timeout in ms', '10000')
689
+ .option('--wait-until <event>', 'Wait until event (load|networkidle|commit)', 'load')
690
+ .action(async (sessionId, opts) => {
691
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/reload`, { timeout_ms: parseInt(opts.timeoutMs), wait_until: opts.waitUntil });
692
+ if (res.error) {
693
+ console.error('Error:', res.error);
694
+ process.exit(1);
695
+ }
696
+ console.log(`✓ Reloaded → ${res.url} (${res.duration_ms}ms)`);
697
+ });
698
+ program
699
+ .command('wait-text <session-id> <text>')
700
+ .description('Wait for text to appear on the page')
701
+ .option('--timeout-ms <ms>', 'Timeout in ms', '5000')
702
+ .action(async (sessionId, text, opts) => {
703
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/wait_text`, { text, timeout_ms: parseInt(opts.timeoutMs) });
704
+ if (res.error) {
705
+ console.error('Error:', res.error);
706
+ process.exit(1);
707
+ }
708
+ console.log(`✓ Text "${text}" appeared (${res.duration_ms}ms)`);
709
+ });
710
+ program
711
+ .command('wait-load-state <session-id>')
712
+ .description('Wait for a specific page load state (load|networkidle|domcontentloaded)')
713
+ .option('--state <state>', 'Load state to wait for', 'load')
714
+ .option('--timeout-ms <ms>', 'Timeout in ms', '10000')
715
+ .action(async (sessionId, opts) => {
716
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/wait_load_state`, { state: opts.state, timeout_ms: parseInt(opts.timeoutMs) });
717
+ if (res.error) {
718
+ console.error('Error:', res.error);
719
+ process.exit(1);
720
+ }
721
+ console.log(`✓ Load state "${res.state}" on ${res.url} (${res.duration_ms}ms)`);
722
+ });
723
+ program
724
+ .command('wait-function <session-id> <expression>')
725
+ .description('Wait until a JS expression returns truthy')
726
+ .option('--timeout-ms <ms>', 'Timeout in ms', '5000')
727
+ .action(async (sessionId, expression, opts) => {
728
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/wait_function`, { expression, timeout_ms: parseInt(opts.timeoutMs) });
729
+ if (res.error) {
730
+ console.error('Error:', res.error);
731
+ process.exit(1);
732
+ }
733
+ console.log(`✓ Function resolved on ${res.url} (${res.duration_ms}ms)`);
734
+ });
735
+ // ---------------------------------------------------------------------------
736
+ // R07-T08 — scroll primitives
737
+ // ---------------------------------------------------------------------------
738
+ program
739
+ .command('scroll-until <session-id>')
740
+ .description('Scroll the page until a stop condition is met')
741
+ .option('--direction <dir>', 'Scroll direction: down|up|left|right', 'down')
742
+ .option('--scroll-selector <sel>', 'CSS selector of element to scroll (default: page body)')
743
+ .option('--stop-selector <sel>', 'Stop when this selector becomes visible')
744
+ .option('--stop-text <text>', 'Stop when this text appears on page')
745
+ .option('--max-scrolls <n>', 'Maximum scroll steps', '50')
746
+ .option('--scroll-delta <px>', 'Pixels per scroll step', '300')
747
+ .option('--stall-ms <ms>', 'Stop if page height unchanged for this many ms', '1500')
748
+ .action(async (sessionId, opts) => {
749
+ const body = {
750
+ direction: opts.direction,
751
+ max_scrolls: parseInt(opts.maxScrolls),
752
+ scroll_delta: parseInt(opts.scrollDelta),
753
+ stall_ms: parseInt(opts.stallMs),
754
+ };
755
+ if (opts.scrollSelector)
756
+ body.scroll_selector = opts.scrollSelector;
757
+ if (opts.stopSelector)
758
+ body.stop_selector = opts.stopSelector;
759
+ if (opts.stopText)
760
+ body.stop_text = opts.stopText;
761
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/scroll_until`, body);
762
+ if (res.error) {
763
+ console.error('Error:', res.error);
764
+ process.exit(1);
765
+ }
766
+ console.log(`✓ Scroll done — ${res.scrolls_performed} scrolls, stopped: ${res.stop_reason} (${res.duration_ms}ms)`);
767
+ });
768
+ // ---------------------------------------------------------------------------
769
+ // R07-T05 — Cookie and storage state
770
+ // ---------------------------------------------------------------------------
771
+ program
772
+ .command('cookie-list <session-id>')
773
+ .description('List all cookies for a session')
774
+ .option('--json', 'Output raw JSON')
775
+ .option('--urls <csv>', 'Filter by comma-separated URL list')
776
+ .action(async (sessionId, opts) => {
777
+ const qs = opts.urls ? `?urls=${encodeURIComponent(opts.urls)}` : '';
778
+ const res = await (0, client_1.apiGet)(`/api/v1/sessions/${sessionId}/cookies${qs}`);
779
+ if (res.error) {
780
+ console.error('Error:', res.error);
781
+ process.exit(1);
782
+ }
783
+ if (opts.json) {
784
+ console.log(JSON.stringify(res, null, 2));
785
+ return;
786
+ }
787
+ const cookies = res.cookies ?? [];
788
+ console.log(`${cookies.length} cookie(s) for session ${sessionId}:`);
789
+ for (const c of cookies)
790
+ console.log(` ${c.name}=${String(c.value).slice(0, 40)} domain=${c.domain} path=${c.path}`);
791
+ });
792
+ program
793
+ .command('cookie-clear <session-id>')
794
+ .description('Clear all cookies for a session')
795
+ .action(async (sessionId) => {
796
+ const { statusCode } = await (0, client_1.apiDelete)(`/api/v1/sessions/${sessionId}/cookies`);
797
+ if (statusCode >= 400) {
798
+ console.error(`Error: HTTP ${statusCode}`);
799
+ process.exit(1);
800
+ }
801
+ console.log(`✓ Cookies cleared for session ${sessionId}`);
802
+ });
803
+ program
804
+ .command('storage-export <session-id>')
805
+ .description('Export the full Playwright storageState (cookies + origins) as JSON')
806
+ .option('-o, --out <file>', 'Save to file instead of stdout')
807
+ .action(async (sessionId, opts) => {
808
+ const res = await (0, client_1.apiGet)(`/api/v1/sessions/${sessionId}/storage_state`);
809
+ if (res.error) {
810
+ console.error('Error:', res.error);
811
+ process.exit(1);
812
+ }
813
+ const json = JSON.stringify(res.storage_state, null, 2);
814
+ if (opts.out) {
815
+ fs_1.default.writeFileSync(opts.out, json);
816
+ console.log(`✓ Storage state saved to ${opts.out}`);
817
+ }
818
+ else
819
+ console.log(json);
820
+ });
821
+ program
822
+ .command('storage-import <session-id> <file>')
823
+ .description('Restore cookies from a previously exported storageState JSON file')
824
+ .action(async (sessionId, file) => {
825
+ const storage_state = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
826
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/storage_state`, { storage_state });
827
+ if (res.error) {
828
+ console.error('Error:', res.error);
829
+ process.exit(1);
830
+ }
831
+ console.log(`✓ Restored ${res.cookies_restored} cookie(s) for session ${sessionId}`);
832
+ });
833
+ // ---------------------------------------------------------------------------
834
+ // R07-T15 — Annotated screenshot
835
+ // ---------------------------------------------------------------------------
836
+ program
837
+ .command('annotated-screenshot <session-id>')
838
+ .description('Take a screenshot with CSS highlight overlays on selected elements')
839
+ .option('-o, --out <file>', 'Output file path', './annotated.png')
840
+ .option('--highlight <selector>', 'CSS selector to highlight (repeatable)', collectValues, [])
841
+ .option('--color <css-color>', 'Highlight color (CSS)', 'rgba(255,80,80,0.35)')
842
+ .option('--format <fmt>', 'png|jpeg', 'png')
843
+ .option('--full-page', 'Capture full page')
844
+ .action(async (sessionId, opts) => {
845
+ const highlights = opts.highlight.map((selector) => ({ selector, color: opts.color }));
846
+ if (highlights.length === 0) {
847
+ console.error('Error: at least one --highlight <selector> is required');
848
+ process.exit(1);
849
+ }
850
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/annotated_screenshot`, {
851
+ highlights, format: opts.format, full_page: opts.fullPage,
852
+ });
853
+ if (res.error) {
854
+ console.error('Error:', res.error);
855
+ process.exit(1);
856
+ }
857
+ const buf = Buffer.from(res.data, 'base64');
858
+ fs_1.default.writeFileSync(opts.out, buf);
859
+ console.log(`✓ Annotated screenshot saved to ${opts.out} (${highlights.length} highlight(s), ${(buf.length / 1024).toFixed(1)}KB, ${res.duration_ms}ms)`);
860
+ });
861
+ // ---------------------------------------------------------------------------
862
+ // R07-T16/T17 — Observability: console log + page errors
863
+ // ---------------------------------------------------------------------------
864
+ program
865
+ .command('console-log <session-id>')
866
+ .description('Show collected browser console log entries')
867
+ .option('--tail <n>', 'Last N entries', '50')
868
+ .option('--json', 'Output raw JSON')
869
+ .action(async (sessionId, opts) => {
870
+ const res = await (0, client_1.apiGet)(`/api/v1/sessions/${sessionId}/console?tail=${opts.tail}`);
871
+ if (res.error) {
872
+ console.error('Error:', res.error);
873
+ process.exit(1);
874
+ }
875
+ if (opts.json) {
876
+ console.log(JSON.stringify(res, null, 2));
877
+ return;
878
+ }
879
+ const entries = res.entries ?? [];
880
+ if (entries.length === 0) {
881
+ console.log('(no console entries)');
882
+ return;
883
+ }
884
+ for (const e of entries)
885
+ console.log(`[${e.ts}] ${e.type} ${e.text}`);
886
+ });
887
+ program
888
+ .command('page-errors <session-id>')
889
+ .description('Show collected uncaught page errors')
890
+ .option('--tail <n>', 'Last N entries', '20')
891
+ .option('--json', 'Output raw JSON')
892
+ .action(async (sessionId, opts) => {
893
+ const res = await (0, client_1.apiGet)(`/api/v1/sessions/${sessionId}/page_errors?tail=${opts.tail}`);
894
+ if (res.error) {
895
+ console.error('Error:', res.error);
896
+ process.exit(1);
897
+ }
898
+ if (opts.json) {
899
+ console.log(JSON.stringify(res, null, 2));
900
+ return;
901
+ }
902
+ const entries = res.entries ?? [];
903
+ if (entries.length === 0) {
904
+ console.log('(no page errors)');
905
+ return;
906
+ }
907
+ for (const e of entries)
908
+ console.log(`[${e.ts}] ERROR ${e.message} url=${e.url}`);
909
+ });
910
+ program
911
+ .command('load-more-until <session-id> <load-more-selector> <content-selector>')
912
+ .description('Repeatedly click a "Load More" button until content count or text condition is met')
913
+ .option('--item-count <n>', 'Stop when at least N items matching content-selector are loaded')
914
+ .option('--stop-text <text>', 'Stop when this text appears on page')
915
+ .option('--max-loads <n>', 'Maximum number of load-more clicks', '20')
916
+ .option('--stall-ms <ms>', 'Stop if item count unchanged for this many ms', '2000')
917
+ .action(async (sessionId, loadMoreSelector, contentSelector, opts) => {
918
+ const body = {
919
+ load_more_selector: loadMoreSelector,
920
+ content_selector: contentSelector,
921
+ max_loads: parseInt(opts.maxLoads),
922
+ stall_ms: parseInt(opts.stallMs),
923
+ };
924
+ if (opts.itemCount)
925
+ body.item_count = parseInt(opts.itemCount);
926
+ if (opts.stopText)
927
+ body.stop_text = opts.stopText;
928
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/load_more_until`, body);
929
+ if (res.error) {
930
+ console.error('Error:', res.error);
931
+ process.exit(1);
932
+ }
933
+ console.log(`✓ Load-more done — ${res.loads_performed} loads, ${res.final_count} items, stopped: ${res.stop_reason} (${res.duration_ms}ms)`);
934
+ });
935
+ // ---------------------------------------------------------------------------
936
+ // R07-T19 — Coordinate-based input primitives
937
+ // ---------------------------------------------------------------------------
938
+ program
939
+ .command('click-at <session-id> <x> <y>')
940
+ .description('Click at pixel coordinates (x, y) — bypasses selector resolution')
941
+ .option('--button <btn>', 'Mouse button: left|right|middle', 'left')
942
+ .option('--click-count <n>', 'Number of clicks', '1')
943
+ .option('--delay-ms <ms>', 'Delay between mousedown and mouseup (ms)', '0')
944
+ .action(async (sessionId, x, y, opts) => {
945
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/click_at`, {
946
+ x: parseFloat(x), y: parseFloat(y),
947
+ button: opts.button, click_count: parseInt(opts.clickCount), delay_ms: parseInt(opts.delayMs),
948
+ });
949
+ if (res.error) {
950
+ printDiagnostics(res);
951
+ process.exit(1);
952
+ }
953
+ console.log(`✓ Clicked at (${res.x}, ${res.y}) (${res.duration_ms}ms)`);
954
+ });
955
+ program
956
+ .command('wheel <session-id>')
957
+ .description('Dispatch a mouse wheel event at the current cursor position')
958
+ .option('--dx <px>', 'Horizontal scroll delta', '0')
959
+ .option('--dy <px>', 'Vertical scroll delta', '300')
960
+ .action(async (sessionId, opts) => {
961
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/wheel`, {
962
+ dx: parseFloat(opts.dx), dy: parseFloat(opts.dy),
963
+ });
964
+ if (res.error) {
965
+ printDiagnostics(res);
966
+ process.exit(1);
967
+ }
968
+ console.log(`✓ Wheel (dx=${res.dx}, dy=${res.dy}) (${res.duration_ms}ms)`);
969
+ });
970
+ program
971
+ .command('insert-text <session-id> <text>')
972
+ .description('Insert text into the focused element, bypassing key events (supports emoji/CJK)')
973
+ .action(async (sessionId, text) => {
974
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/insert_text`, { text });
975
+ if (res.error) {
976
+ printDiagnostics(res);
977
+ process.exit(1);
978
+ }
979
+ console.log(`✓ Inserted text (${res.length} chars, ${res.duration_ms}ms)`);
980
+ });
981
+ // ---------------------------------------------------------------------------
982
+ // R07-T20 — Bounding box
983
+ // ---------------------------------------------------------------------------
984
+ program
985
+ .command('bbox <session-id> <selector-or-eid>')
986
+ .description('Return the bounding box of an element (selector or element_id)')
987
+ .option('--element-id', 'Treat arg as element_id from element-map')
988
+ .action(async (sessionId, target, opts) => {
989
+ const body = opts.elementId ? { element_id: target } : { selector: target };
990
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/bbox`, body);
991
+ if (res.error) {
992
+ console.error('Error:', res.error);
993
+ process.exit(1);
994
+ }
995
+ if (!res.found) {
996
+ console.log(`(element not found: "${target}")`);
997
+ return;
998
+ }
999
+ console.log(`✓ Bbox: x=${res.x} y=${res.y} w=${res.width} h=${res.height} center=(${res.center_x}, ${res.center_y}) (${res.duration_ms}ms)`);
1000
+ });
1001
+ // ---------------------------------------------------------------------------
1002
+ // R07-T22 — Dialog observability
1003
+ // ---------------------------------------------------------------------------
1004
+ program
1005
+ .command('dialogs <session-id>')
1006
+ .description('List auto-dismissed dialog history for a session')
1007
+ .option('--tail <n>', 'Last N entries')
1008
+ .option('--clear', 'Clear the dialog history buffer')
1009
+ .action(async (sessionId, opts) => {
1010
+ if (opts.clear) {
1011
+ const r = await (0, client_1.apiDelete)(`/api/v1/sessions/${sessionId}/dialogs`);
1012
+ console.log(`✓ Dialog history cleared (status ${r.statusCode})`);
1013
+ return;
1014
+ }
1015
+ const qs = opts.tail ? `?tail=${opts.tail}` : '';
1016
+ const res = await (0, client_1.apiGet)(`/api/v1/sessions/${sessionId}/dialogs${qs}`);
1017
+ if (res.error) {
1018
+ console.error('Error:', res.error);
1019
+ process.exit(1);
1020
+ }
1021
+ if (res.count === 0) {
1022
+ console.log('(no dialogs)');
1023
+ return;
1024
+ }
1025
+ for (const e of res.entries) {
1026
+ console.log(`[${e.ts}] ${e.type} "${e.message}" (${e.action}) url=${e.url}`);
1027
+ }
1028
+ });
1029
+ // ---------------------------------------------------------------------------
1030
+ // R07-T23 — Clipboard
1031
+ // ---------------------------------------------------------------------------
1032
+ program
1033
+ .command('clipboard-write <session-id> <text>')
1034
+ .description('Write text to the browser clipboard')
1035
+ .action(async (sessionId, text) => {
1036
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/clipboard`, { text });
1037
+ if (res.error) {
1038
+ printDiagnostics(res);
1039
+ process.exit(1);
1040
+ }
1041
+ console.log(`✓ Clipboard written (${res.length} chars, ${res.duration_ms}ms)`);
1042
+ });
1043
+ program
1044
+ .command('clipboard-read <session-id>')
1045
+ .description('Read text from the browser clipboard')
1046
+ .action(async (sessionId) => {
1047
+ const res = await (0, client_1.apiGet)(`/api/v1/sessions/${sessionId}/clipboard`);
1048
+ if (res.error) {
1049
+ printDiagnostics(res);
1050
+ process.exit(1);
1051
+ }
1052
+ console.log(res.text);
1053
+ });
1054
+ // ---------------------------------------------------------------------------
1055
+ // R07-T24 — Viewport emulation
1056
+ // ---------------------------------------------------------------------------
1057
+ program
1058
+ .command('set-viewport <session-id> <width> <height>')
1059
+ .description('Resize the page viewport to width × height pixels')
1060
+ .action(async (sessionId, width, height) => {
1061
+ const res = await (0, client_1.apiPut)(`/api/v1/sessions/${sessionId}/viewport`, {
1062
+ width: parseInt(width), height: parseInt(height),
1063
+ });
1064
+ if (res.error) {
1065
+ console.error('Error:', res.error);
1066
+ process.exit(1);
1067
+ }
1068
+ console.log(`✓ Viewport set to ${res.width}×${res.height} (${res.duration_ms}ms)`);
1069
+ });
1070
+ // ---------------------------------------------------------------------------
1071
+ // R07-T25 — Network conditions
1072
+ // ---------------------------------------------------------------------------
1073
+ program
1074
+ .command('set-network <session-id>')
1075
+ .description('Emulate network throttling or offline mode (CDP)')
1076
+ .option('--offline', 'Enable offline mode')
1077
+ .option('--latency-ms <ms>', 'Additional latency in ms', '0')
1078
+ .option('--download-kbps <kbps>', 'Download bandwidth limit (-1 = unlimited)', '-1')
1079
+ .option('--upload-kbps <kbps>', 'Upload bandwidth limit (-1 = unlimited)', '-1')
1080
+ .action(async (sessionId, opts) => {
1081
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/network_conditions`, {
1082
+ offline: !!opts.offline,
1083
+ latency_ms: parseInt(opts.latencyMs),
1084
+ download_kbps: parseFloat(opts.downloadKbps),
1085
+ upload_kbps: parseFloat(opts.uploadKbps),
1086
+ });
1087
+ if (res.error) {
1088
+ console.error('Error:', res.error);
1089
+ process.exit(1);
1090
+ }
1091
+ console.log(`✓ Network: offline=${res.offline} latency=${res.latency_ms}ms down=${res.download_kbps} up=${res.upload_kbps}`);
1092
+ });
1093
+ program
1094
+ .command('reset-network <session-id>')
1095
+ .description('Reset network conditions to normal (no throttling)')
1096
+ .action(async (sessionId) => {
1097
+ const r = await (0, client_1.apiDelete)(`/api/v1/sessions/${sessionId}/network_conditions`);
1098
+ console.log(`✓ Network conditions reset (status ${r.statusCode})`);
1099
+ });
348
1100
  }
349
1101
  //# sourceMappingURL=actions.js.map