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.
- package/README.md +82 -1
- package/dist/browser/actions.d.ts +272 -0
- package/dist/browser/actions.d.ts.map +1 -1
- package/dist/browser/actions.js +797 -0
- package/dist/browser/actions.js.map +1 -1
- package/dist/browser/manager.d.ts +88 -0
- package/dist/browser/manager.d.ts.map +1 -1
- package/dist/browser/manager.js +231 -0
- package/dist/browser/manager.js.map +1 -1
- package/dist/cli/client.d.ts +1 -0
- package/dist/cli/client.d.ts.map +1 -1
- package/dist/cli/client.js +21 -0
- package/dist/cli/client.js.map +1 -1
- package/dist/cli/commands/actions.d.ts.map +1 -1
- package/dist/cli/commands/actions.js +762 -10
- package/dist/cli/commands/actions.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/daemon/routes/actions.d.ts.map +1 -1
- package/dist/daemon/routes/actions.js +529 -6
- package/dist/daemon/routes/actions.js.map +1 -1
- package/dist/daemon/routes/browser_control.d.ts +12 -0
- package/dist/daemon/routes/browser_control.d.ts.map +1 -0
- package/dist/daemon/routes/browser_control.js +172 -0
- package/dist/daemon/routes/browser_control.js.map +1 -0
- package/dist/daemon/routes/interaction.d.ts +11 -0
- package/dist/daemon/routes/interaction.d.ts.map +1 -0
- package/dist/daemon/routes/interaction.js +176 -0
- package/dist/daemon/routes/interaction.js.map +1 -0
- package/dist/daemon/routes/state.d.ts +11 -0
- package/dist/daemon/routes/state.d.ts.map +1 -0
- package/dist/daemon/routes/state.js +190 -0
- package/dist/daemon/routes/state.js.map +1 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +7 -1
- package/dist/daemon/server.js.map +1 -1
- 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
|
-
.
|
|
93
|
-
|
|
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 "${
|
|
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
|
-
.
|
|
104
|
-
|
|
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 "${
|
|
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
|