agentmb 0.1.1 → 0.3.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 (41) hide show
  1. package/README.md +818 -153
  2. package/dist/browser/actions.d.ts +24 -1
  3. package/dist/browser/actions.d.ts.map +1 -1
  4. package/dist/browser/actions.js +118 -19
  5. package/dist/browser/actions.js.map +1 -1
  6. package/dist/browser/manager.d.ts +14 -0
  7. package/dist/browser/manager.d.ts.map +1 -1
  8. package/dist/browser/manager.js +117 -4
  9. package/dist/browser/manager.js.map +1 -1
  10. package/dist/cli/commands/actions.d.ts.map +1 -1
  11. package/dist/cli/commands/actions.js +305 -70
  12. package/dist/cli/commands/actions.js.map +1 -1
  13. package/dist/cli/commands/browser-launch.d.ts +7 -0
  14. package/dist/cli/commands/browser-launch.d.ts.map +1 -0
  15. package/dist/cli/commands/browser-launch.js +116 -0
  16. package/dist/cli/commands/browser-launch.js.map +1 -0
  17. package/dist/cli/commands/session.d.ts.map +1 -1
  18. package/dist/cli/commands/session.js +67 -4
  19. package/dist/cli/commands/session.js.map +1 -1
  20. package/dist/cli/index.js +3 -1
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/daemon/index.js +2 -2
  23. package/dist/daemon/index.js.map +1 -1
  24. package/dist/daemon/routes/actions.d.ts.map +1 -1
  25. package/dist/daemon/routes/actions.js +419 -66
  26. package/dist/daemon/routes/actions.js.map +1 -1
  27. package/dist/daemon/routes/sessions.d.ts.map +1 -1
  28. package/dist/daemon/routes/sessions.js +208 -3
  29. package/dist/daemon/routes/sessions.js.map +1 -1
  30. package/dist/daemon/routes/state.d.ts.map +1 -1
  31. package/dist/daemon/routes/state.js +26 -0
  32. package/dist/daemon/routes/state.js.map +1 -1
  33. package/dist/daemon/server.js +1 -1
  34. package/dist/daemon/session.d.ts +19 -0
  35. package/dist/daemon/session.d.ts.map +1 -1
  36. package/dist/daemon/session.js +13 -0
  37. package/dist/daemon/session.js.map +1 -1
  38. package/dist/policy/types.d.ts.map +1 -1
  39. package/dist/policy/types.js +14 -12
  40. package/dist/policy/types.js.map +1 -1
  41. package/package.json +1 -1
@@ -38,6 +38,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.registerActionRoutes = registerActionRoutes;
40
40
  const crypto_1 = __importDefault(require("crypto"));
41
+ const fs_1 = __importDefault(require("fs"));
42
+ const os_1 = __importDefault(require("os"));
43
+ const path_1 = __importDefault(require("path"));
41
44
  require("../types"); // T11: Fastify type augmentation
42
45
  const Actions = __importStar(require("../../browser/actions"));
43
46
  const actions_1 = require("../../browser/actions");
@@ -146,6 +149,59 @@ async function applyPolicy(server, sessionId, domain, action, opts, reply) {
146
149
  }
147
150
  return true;
148
151
  }
152
+ function pfRange(field, value, min, max) {
153
+ if (value === undefined)
154
+ return null;
155
+ return value < min || value > max ? { field, constraint: `must be ${min}–${max}`, value } : null;
156
+ }
157
+ function pfMaxLen(field, value, max) {
158
+ if (!value)
159
+ return null;
160
+ return value.length > max ? { field, constraint: `max length ${max} chars`, value: value.length } : null;
161
+ }
162
+ /** Run an ordered list of checks; sends 400 + returns false on first violation. */
163
+ function preflight(checks, reply) {
164
+ const v = checks.find(Boolean);
165
+ if (v) {
166
+ reply.code(400).send({ error: 'preflight_failed', field: v.field, constraint: v.constraint, value: v.value });
167
+ return false;
168
+ }
169
+ return true;
170
+ }
171
+ async function applyStabilityPre(page, opts) {
172
+ if (!opts)
173
+ return;
174
+ if (opts.wait_before_ms)
175
+ await new Promise(r => setTimeout(r, opts.wait_before_ms));
176
+ if (opts.wait_dom_stable_ms) {
177
+ try {
178
+ await page.waitForFunction('document.readyState === "complete"', undefined, { timeout: opts.wait_dom_stable_ms });
179
+ }
180
+ catch { /* timeout is acceptable */ }
181
+ }
182
+ }
183
+ async function applyStabilityPost(page, opts) {
184
+ if (!opts)
185
+ return;
186
+ if (opts.wait_after_ms)
187
+ await new Promise(r => setTimeout(r, opts.wait_after_ms));
188
+ }
189
+ // ---------------------------------------------------------------------------
190
+ // R08-R13: Error recovery hints — enrich 422 ActionDiagnostics with hint
191
+ // ---------------------------------------------------------------------------
192
+ function enrichDiag(diag) {
193
+ const msg = diag.error.toLowerCase();
194
+ let recovery_hint;
195
+ if (msg.includes('timeout') || msg.includes('waiting for'))
196
+ recovery_hint = 'Increase timeout_ms or add stability.wait_before_ms; ensure element is visible before acting';
197
+ else if (msg.includes('target closed') || msg.includes('execution context') || msg.includes('detached'))
198
+ recovery_hint = 'Page may have navigated or element was removed; re-navigate or re-snapshot';
199
+ else if (msg.includes('not found') || msg.includes('no element') || msg.includes('failed to find'))
200
+ recovery_hint = 'Check selector; use snapshot_map to verify element exists on current page';
201
+ else if (msg.includes('intercept') || msg.includes('overlap') || msg.includes('obscur'))
202
+ recovery_hint = 'Element may be covered by overlay; try executor=auto_fallback or scroll into view first';
203
+ return recovery_hint ? { ...diag, recovery_hint } : diag;
204
+ }
149
205
  function registerActionRoutes(server, registry) {
150
206
  function getLogger() {
151
207
  return server.auditLogger;
@@ -172,7 +228,11 @@ function registerActionRoutes(server, registry) {
172
228
  const eid = input.ref_id.slice(colonIdx + 1);
173
229
  const snapshot = bm.getSnapshot(sessionId, snapshotId);
174
230
  if (!snapshot) {
175
- reply.code(409).send({ error: 'stale_ref', ref_id: input.ref_id, message: 'Snapshot not found or expired; call snapshot_map again' });
231
+ reply.code(409).send({
232
+ error: 'stale_ref', ref_id: input.ref_id,
233
+ message: 'Snapshot not found or expired; call snapshot_map again',
234
+ suggestions: ['call snapshot_map to get fresh ref_ids', 'use selector or element_id as fallback'],
235
+ });
176
236
  return null;
177
237
  }
178
238
  const currentRev = bm.getPageRev(sessionId);
@@ -183,6 +243,7 @@ function registerActionRoutes(server, registry) {
183
243
  snapshot_page_rev: snapshot.page_rev,
184
244
  current_page_rev: currentRev,
185
245
  message: 'Page has changed since snapshot was taken; call snapshot_map again',
246
+ suggestions: ['call snapshot_map to get fresh ref_ids', 'if page is stable, the navigation event incremented page_rev — wait and resnapshot'],
186
247
  });
187
248
  return null;
188
249
  }
@@ -225,12 +286,16 @@ function registerActionRoutes(server, registry) {
225
286
  return Actions.navigate(s.page, url, wait_until, getLogger(), s.id, purpose, inferOperator(req, s, operator));
226
287
  });
227
288
  // POST /api/v1/sessions/:id/click
228
- // T21: add fallback_x / fallback_y if DOM click fails, retry via page.mouse.click()
289
+ // R08-R06: executor='auto_fallback' automatically retries via bbox coords on DOM failure
290
+ // R08-R02: stability.wait_before_ms / wait_after_ms / wait_dom_stable_ms
291
+ // R08-R09: preflight validates timeout_ms range
229
292
  server.post('/api/v1/sessions/:id/click', async (req, reply) => {
230
293
  const s = resolve(req.params.id, reply);
231
294
  if (!s)
232
295
  return;
233
- const { timeout_ms = 5000, frame, purpose, operator, sensitive, retry, fallback_x, fallback_y } = req.body;
296
+ const { timeout_ms = 5000, frame, purpose, operator, sensitive, retry, fallback_x, fallback_y, executor = 'strict', stability } = req.body;
297
+ if (!preflight([pfRange('timeout_ms', timeout_ms, 50, 60000)], reply))
298
+ return;
234
299
  const selector = resolveTarget(req.body, reply, s.id);
235
300
  if (!selector)
236
301
  return;
@@ -239,32 +304,63 @@ function registerActionRoutes(server, registry) {
239
304
  const target = resolveOrReply(s.page, frame, reply);
240
305
  if (!target)
241
306
  return;
307
+ await applyStabilityPre(s.page, stability);
242
308
  try {
243
- return await Actions.click(target, selector, timeout_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator));
309
+ const result = await Actions.click(target, selector, timeout_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator));
310
+ await applyStabilityPost(s.page, stability);
311
+ return { ...result, executed_via: 'high_level' };
244
312
  }
245
313
  catch (domErr) {
246
- // T21 dual-track: if fallback coordinates provided, retry via mouse.click()
247
- if (fallback_x !== undefined && fallback_y !== undefined && s.page) {
314
+ // auto_fallback: resolve element bbox and retry via mouse.click()
315
+ if (executor === 'auto_fallback') {
248
316
  try {
249
- await s.page.mouse.click(fallback_x, fallback_y);
250
- const duration_ms = 0;
251
- return { status: 'ok', selector, track: 'coords', fallback_x, fallback_y, duration_ms };
317
+ const bbox = await target.locator(selector).boundingBox();
318
+ if (bbox) {
319
+ let cx = Math.round(bbox.x + bbox.width / 2);
320
+ let cy = Math.round(bbox.y + bbox.height / 2);
321
+ // Frame offset compensation: when target is a Frame (not the Page),
322
+ // boundingBox() coords are relative to the frame viewport; add the
323
+ // frame element's page-level offset so mouse.click lands correctly.
324
+ if (frame && target !== s.page) {
325
+ try {
326
+ const frameRect = await target.evaluate('(() => { const el = window.frameElement; if (!el) return {x:0,y:0}; const r = el.getBoundingClientRect(); return {x: r.x, y: r.y}; })()');
327
+ cx += Math.round(frameRect.x);
328
+ cy += Math.round(frameRect.y);
329
+ }
330
+ catch { /* best-effort; proceed with uncompensated coords */ }
331
+ }
332
+ await s.page.mouse.click(cx, cy);
333
+ await applyStabilityPost(s.page, stability);
334
+ return { status: 'ok', selector, executed_via: 'low_level', fallback_x: cx, fallback_y: cy, duration_ms: 0 };
335
+ }
252
336
  }
253
- catch (coordErr) {
254
- // Both tracks failed — report original DOM error
337
+ catch { /* fallback also failed — fall through to original error */ }
338
+ }
339
+ // explicit fallback coordinates (legacy T21 path)
340
+ if (fallback_x !== undefined && fallback_y !== undefined) {
341
+ try {
342
+ await s.page.mouse.click(fallback_x, fallback_y);
343
+ return { status: 'ok', selector, executed_via: 'low_level', track: 'coords', fallback_x, fallback_y, duration_ms: 0 };
255
344
  }
345
+ catch { /* both tracks failed */ }
346
+ }
347
+ if (domErr instanceof Actions.ActionDiagnosticsError) {
348
+ const diag = enrichDiag({ ...domErr.diagnostics, suggested_fallback: 'retry with executor="auto_fallback" to attempt coordinates-based click' });
349
+ return reply.code(422).send(diag);
256
350
  }
257
- if (domErr instanceof Actions.ActionDiagnosticsError)
258
- return reply.code(422).send(domErr.diagnostics);
259
351
  throw domErr;
260
352
  }
261
353
  });
262
354
  // POST /api/v1/sessions/:id/fill
355
+ // R08-R01: fill_strategy='type' uses pressSequentially with char_delay_ms (humanization)
356
+ // R08-R02: stability options; R08-R09: preflight value length
263
357
  server.post('/api/v1/sessions/:id/fill', async (req, reply) => {
264
358
  const s = resolve(req.params.id, reply);
265
359
  if (!s)
266
360
  return;
267
- const { value, frame, purpose, operator, sensitive, retry } = req.body;
361
+ const { value, frame, purpose, operator, sensitive, retry, stability, fill_strategy = 'instant', char_delay_ms = 0 } = req.body;
362
+ if (!preflight([pfMaxLen('value', value, 100_000)], reply))
363
+ return;
268
364
  const selector = resolveTarget(req.body, reply, s.id);
269
365
  if (!selector)
270
366
  return;
@@ -273,7 +369,12 @@ function registerActionRoutes(server, registry) {
273
369
  const target = resolveOrReply(s.page, frame, reply);
274
370
  if (!target)
275
371
  return;
276
- return Actions.fill(target, selector, value, getLogger(), s.id, purpose, inferOperator(req, s, operator));
372
+ await applyStabilityPre(s.page, stability);
373
+ const result = fill_strategy === 'type'
374
+ ? await Actions.typeText(target, selector, value, char_delay_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator))
375
+ : await Actions.fill(target, selector, value, getLogger(), s.id, purpose, inferOperator(req, s, operator));
376
+ await applyStabilityPost(s.page, stability);
377
+ return result;
277
378
  });
278
379
  // POST /api/v1/sessions/:id/eval
279
380
  server.post('/api/v1/sessions/:id/eval', async (req, reply) => {
@@ -291,7 +392,7 @@ function registerActionRoutes(server, registry) {
291
392
  }
292
393
  catch (e) {
293
394
  if (e instanceof actions_1.ActionDiagnosticsError)
294
- return reply.code(422).send(e.diagnostics);
395
+ return reply.code(422).send(enrichDiag(e.diagnostics));
295
396
  throw e;
296
397
  }
297
398
  });
@@ -311,7 +412,7 @@ function registerActionRoutes(server, registry) {
311
412
  }
312
413
  catch (e) {
313
414
  if (e instanceof actions_1.ActionDiagnosticsError)
314
- return reply.code(422).send(e.diagnostics);
415
+ return reply.code(422).send(enrichDiag(e.diagnostics));
315
416
  throw e;
316
417
  }
317
418
  });
@@ -326,7 +427,7 @@ function registerActionRoutes(server, registry) {
326
427
  }
327
428
  catch (e) {
328
429
  if (e instanceof actions_1.ActionDiagnosticsError)
329
- return reply.code(422).send(e.diagnostics);
430
+ return reply.code(422).send(enrichDiag(e.diagnostics));
330
431
  throw e;
331
432
  }
332
433
  });
@@ -350,7 +451,7 @@ function registerActionRoutes(server, registry) {
350
451
  }
351
452
  catch (e) {
352
453
  if (e instanceof actions_1.ActionDiagnosticsError)
353
- return reply.code(422).send(e.diagnostics);
454
+ return reply.code(422).send(enrichDiag(e.diagnostics));
354
455
  throw e;
355
456
  }
356
457
  });
@@ -373,7 +474,7 @@ function registerActionRoutes(server, registry) {
373
474
  }
374
475
  catch (e) {
375
476
  if (e instanceof actions_1.ActionDiagnosticsError)
376
- return reply.code(422).send(e.diagnostics);
477
+ return reply.code(422).send(enrichDiag(e.diagnostics));
377
478
  throw e;
378
479
  }
379
480
  });
@@ -391,7 +492,7 @@ function registerActionRoutes(server, registry) {
391
492
  }
392
493
  catch (e) {
393
494
  if (e instanceof actions_1.ActionDiagnosticsError)
394
- return reply.code(422).send(e.diagnostics);
495
+ return reply.code(422).send(enrichDiag(e.diagnostics));
395
496
  throw e;
396
497
  }
397
498
  });
@@ -412,7 +513,7 @@ function registerActionRoutes(server, registry) {
412
513
  }
413
514
  catch (e) {
414
515
  if (e instanceof actions_1.ActionDiagnosticsError)
415
- return reply.code(422).send(e.diagnostics);
516
+ return reply.code(422).send(enrichDiag(e.diagnostics));
416
517
  throw e;
417
518
  }
418
519
  });
@@ -430,7 +531,7 @@ function registerActionRoutes(server, registry) {
430
531
  }
431
532
  catch (e) {
432
533
  if (e instanceof actions_1.ActionDiagnosticsError)
433
- return reply.code(422).send(e.diagnostics);
534
+ return reply.code(422).send(enrichDiag(e.diagnostics));
434
535
  throw e;
435
536
  }
436
537
  });
@@ -445,7 +546,7 @@ function registerActionRoutes(server, registry) {
445
546
  }
446
547
  catch (e) {
447
548
  if (e instanceof actions_1.ActionDiagnosticsError)
448
- return reply.code(422).send(e.diagnostics);
549
+ return reply.code(422).send(enrichDiag(e.diagnostics));
449
550
  throw e;
450
551
  }
451
552
  });
@@ -461,7 +562,7 @@ function registerActionRoutes(server, registry) {
461
562
  }
462
563
  catch (e) {
463
564
  if (e instanceof actions_1.ActionDiagnosticsError)
464
- return reply.code(422).send(e.diagnostics);
565
+ return reply.code(422).send(enrichDiag(e.diagnostics));
465
566
  throw e;
466
567
  }
467
568
  });
@@ -481,22 +582,35 @@ function registerActionRoutes(server, registry) {
481
582
  }
482
583
  catch (e) {
483
584
  if (e instanceof actions_1.ActionDiagnosticsError)
484
- return reply.code(422).send(e.diagnostics);
585
+ return reply.code(422).send(enrichDiag(e.diagnostics));
485
586
  throw e;
486
587
  }
487
588
  });
488
589
  // POST /api/v1/sessions/:id/download
590
+ // T07: guard on accept_downloads; T08: element_id / ref_id support
489
591
  server.post('/api/v1/sessions/:id/download', async (req, reply) => {
490
592
  const s = resolve(req.params.id, reply);
491
593
  if (!s)
492
594
  return;
493
- const { selector, timeout_ms = 30000, max_bytes = 50 * 1024 * 1024, purpose, operator } = req.body;
595
+ // T07: check accept_downloads is enabled
596
+ const bm = server.browserManager;
597
+ if (bm && !bm.getAcceptDownloads(s.id)) {
598
+ return reply.code(422).send({
599
+ error: 'download_not_enabled',
600
+ message: 'Downloads are disabled for this session. Create the session with accept_downloads=true (CLI: agentmb session new --accept-downloads).',
601
+ });
602
+ }
603
+ const { timeout_ms = 30000, max_bytes = 50 * 1024 * 1024, purpose, operator } = req.body;
604
+ // T08: resolve selector/element_id/ref_id to CSS selector
605
+ const selector = resolveTarget(req.body, reply, s.id);
606
+ if (!selector)
607
+ return;
494
608
  try {
495
609
  return await Actions.downloadFile(s.page, selector, timeout_ms, max_bytes, getLogger(), s.id, purpose, inferOperator(req, s, operator));
496
610
  }
497
611
  catch (e) {
498
612
  if (e instanceof actions_1.ActionDiagnosticsError)
499
- return reply.code(422).send(e.diagnostics);
613
+ return reply.code(422).send(enrichDiag(e.diagnostics));
500
614
  throw e;
501
615
  }
502
616
  });
@@ -516,13 +630,13 @@ function registerActionRoutes(server, registry) {
516
630
  const s = resolve(req.params.id, reply);
517
631
  if (!s)
518
632
  return;
519
- const { scope, limit = 500, purpose, operator } = req.body ?? {};
633
+ const { scope, limit = 500, include_unlabeled = false, purpose, operator } = req.body ?? {};
520
634
  try {
521
- return await Actions.elementMap(s.page, { scope, limit }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
635
+ return await Actions.elementMap(s.page, { scope, limit, include_unlabeled }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
522
636
  }
523
637
  catch (e) {
524
638
  if (e instanceof actions_1.ActionDiagnosticsError)
525
- return reply.code(422).send(e.diagnostics);
639
+ return reply.code(422).send(enrichDiag(e.diagnostics));
526
640
  throw e;
527
641
  }
528
642
  });
@@ -547,7 +661,7 @@ function registerActionRoutes(server, registry) {
547
661
  }
548
662
  catch (e) {
549
663
  if (e instanceof actions_1.ActionDiagnosticsError)
550
- return reply.code(422).send(e.diagnostics);
664
+ return reply.code(422).send(enrichDiag(e.diagnostics));
551
665
  throw e;
552
666
  }
553
667
  });
@@ -572,7 +686,7 @@ function registerActionRoutes(server, registry) {
572
686
  }
573
687
  catch (e) {
574
688
  if (e instanceof actions_1.ActionDiagnosticsError)
575
- return reply.code(422).send(e.diagnostics);
689
+ return reply.code(422).send(enrichDiag(e.diagnostics));
576
690
  throw e;
577
691
  }
578
692
  });
@@ -589,7 +703,7 @@ function registerActionRoutes(server, registry) {
589
703
  }
590
704
  catch (e) {
591
705
  if (e instanceof actions_1.ActionDiagnosticsError)
592
- return reply.code(422).send(e.diagnostics);
706
+ return reply.code(422).send(enrichDiag(e.diagnostics));
593
707
  throw e;
594
708
  }
595
709
  });
@@ -611,7 +725,7 @@ function registerActionRoutes(server, registry) {
611
725
  }
612
726
  catch (e) {
613
727
  if (e instanceof actions_1.ActionDiagnosticsError)
614
- return reply.code(422).send(e.diagnostics);
728
+ return reply.code(422).send(enrichDiag(e.diagnostics));
615
729
  throw e;
616
730
  }
617
731
  });
@@ -630,7 +744,7 @@ function registerActionRoutes(server, registry) {
630
744
  }
631
745
  catch (e) {
632
746
  if (e instanceof actions_1.ActionDiagnosticsError)
633
- return reply.code(422).send(e.diagnostics);
747
+ return reply.code(422).send(enrichDiag(e.diagnostics));
634
748
  throw e;
635
749
  }
636
750
  });
@@ -649,7 +763,7 @@ function registerActionRoutes(server, registry) {
649
763
  }
650
764
  catch (e) {
651
765
  if (e instanceof actions_1.ActionDiagnosticsError)
652
- return reply.code(422).send(e.diagnostics);
766
+ return reply.code(422).send(enrichDiag(e.diagnostics));
653
767
  throw e;
654
768
  }
655
769
  });
@@ -668,7 +782,7 @@ function registerActionRoutes(server, registry) {
668
782
  }
669
783
  catch (e) {
670
784
  if (e instanceof actions_1.ActionDiagnosticsError)
671
- return reply.code(422).send(e.diagnostics);
785
+ return reply.code(422).send(enrichDiag(e.diagnostics));
672
786
  throw e;
673
787
  }
674
788
  });
@@ -688,7 +802,7 @@ function registerActionRoutes(server, registry) {
688
802
  }
689
803
  catch (e) {
690
804
  if (e instanceof actions_1.ActionDiagnosticsError)
691
- return reply.code(422).send(e.diagnostics);
805
+ return reply.code(422).send(enrichDiag(e.diagnostics));
692
806
  throw e;
693
807
  }
694
808
  });
@@ -707,7 +821,7 @@ function registerActionRoutes(server, registry) {
707
821
  }
708
822
  catch (e) {
709
823
  if (e instanceof actions_1.ActionDiagnosticsError)
710
- return reply.code(422).send(e.diagnostics);
824
+ return reply.code(422).send(enrichDiag(e.diagnostics));
711
825
  throw e;
712
826
  }
713
827
  });
@@ -715,31 +829,48 @@ function registerActionRoutes(server, registry) {
715
829
  const s = resolve(req.params.id, reply);
716
830
  if (!s)
717
831
  return;
718
- const { source, source_element_id, target, target_element_id, purpose, operator } = req.body;
719
- const src = source_element_id ? `[data-agentmb-eid="${source_element_id}"]` : source;
720
- const tgt = target_element_id ? `[data-agentmb-eid="${target_element_id}"]` : target;
721
- if (!src || !tgt)
722
- return reply.code(400).send({ error: 'source and target are required (selector or element_id)' });
832
+ const { source, source_element_id, source_ref_id, target, target_element_id, target_ref_id, purpose, operator } = req.body;
833
+ const src = resolveTarget({ selector: source, element_id: source_element_id, ref_id: source_ref_id }, reply, s.id);
834
+ if (!src)
835
+ return;
836
+ const tgt = resolveTarget({ selector: target, element_id: target_element_id, ref_id: target_ref_id }, reply, s.id);
837
+ if (!tgt)
838
+ return;
723
839
  try {
724
840
  return await Actions.drag(s.page, src, tgt, getLogger(), s.id, purpose, inferOperator(req, s, operator));
725
841
  }
726
842
  catch (e) {
727
843
  if (e instanceof actions_1.ActionDiagnosticsError)
728
- return reply.code(422).send(e.diagnostics);
844
+ return reply.code(422).send(enrichDiag(e.diagnostics));
729
845
  throw e;
730
846
  }
731
847
  });
848
+ // R08-R05/R08: Ref->Box->Input — mouse_move accepts ref_id + steps for smooth trajectory
732
849
  server.post('/api/v1/sessions/:id/mouse_move', async (req, reply) => {
733
850
  const s = resolve(req.params.id, reply);
734
851
  if (!s)
735
852
  return;
736
- const { x, y, purpose, operator } = req.body;
853
+ const { purpose, operator, steps } = req.body;
854
+ let { x, y } = req.body;
855
+ // Ref->Box->Input: if ref_id/element_id/selector provided, resolve to bbox center coords
856
+ if (req.body.ref_id || req.body.element_id || req.body.selector) {
857
+ const cssSelector = resolveTarget(req.body, reply, s.id);
858
+ if (!cssSelector)
859
+ return;
860
+ const bbox = await s.page.locator(cssSelector).boundingBox();
861
+ if (!bbox)
862
+ return reply.code(404).send({ error: 'Element not found or not visible for mouse_move', selector: cssSelector });
863
+ x = Math.round(bbox.x + bbox.width / 2);
864
+ y = Math.round(bbox.y + bbox.height / 2);
865
+ }
866
+ if (x === undefined || y === undefined)
867
+ return reply.code(400).send({ error: 'Either x+y coordinates or ref_id/element_id/selector is required' });
737
868
  try {
738
- return await Actions.mouseMove(s.page, x, y, getLogger(), s.id, purpose, inferOperator(req, s, operator));
869
+ return await Actions.mouseMove(s.page, x, y, steps ?? 1, getLogger(), s.id, purpose, inferOperator(req, s, operator));
739
870
  }
740
871
  catch (e) {
741
872
  if (e instanceof actions_1.ActionDiagnosticsError)
742
- return reply.code(422).send(e.diagnostics);
873
+ return reply.code(422).send(enrichDiag(e.diagnostics));
743
874
  throw e;
744
875
  }
745
876
  });
@@ -753,7 +884,7 @@ function registerActionRoutes(server, registry) {
753
884
  }
754
885
  catch (e) {
755
886
  if (e instanceof actions_1.ActionDiagnosticsError)
756
- return reply.code(422).send(e.diagnostics);
887
+ return reply.code(422).send(enrichDiag(e.diagnostics));
757
888
  throw e;
758
889
  }
759
890
  });
@@ -767,7 +898,7 @@ function registerActionRoutes(server, registry) {
767
898
  }
768
899
  catch (e) {
769
900
  if (e instanceof actions_1.ActionDiagnosticsError)
770
- return reply.code(422).send(e.diagnostics);
901
+ return reply.code(422).send(enrichDiag(e.diagnostics));
771
902
  throw e;
772
903
  }
773
904
  });
@@ -781,7 +912,7 @@ function registerActionRoutes(server, registry) {
781
912
  }
782
913
  catch (e) {
783
914
  if (e instanceof actions_1.ActionDiagnosticsError)
784
- return reply.code(422).send(e.diagnostics);
915
+ return reply.code(422).send(enrichDiag(e.diagnostics));
785
916
  throw e;
786
917
  }
787
918
  });
@@ -795,7 +926,7 @@ function registerActionRoutes(server, registry) {
795
926
  }
796
927
  catch (e) {
797
928
  if (e instanceof actions_1.ActionDiagnosticsError)
798
- return reply.code(422).send(e.diagnostics);
929
+ return reply.code(422).send(enrichDiag(e.diagnostics));
799
930
  throw e;
800
931
  }
801
932
  });
@@ -812,7 +943,7 @@ function registerActionRoutes(server, registry) {
812
943
  }
813
944
  catch (e) {
814
945
  if (e instanceof actions_1.ActionDiagnosticsError)
815
- return reply.code(422).send(e.diagnostics);
946
+ return reply.code(422).send(enrichDiag(e.diagnostics));
816
947
  throw e;
817
948
  }
818
949
  });
@@ -826,7 +957,7 @@ function registerActionRoutes(server, registry) {
826
957
  }
827
958
  catch (e) {
828
959
  if (e instanceof actions_1.ActionDiagnosticsError)
829
- return reply.code(422).send(e.diagnostics);
960
+ return reply.code(422).send(enrichDiag(e.diagnostics));
830
961
  throw e;
831
962
  }
832
963
  });
@@ -840,7 +971,7 @@ function registerActionRoutes(server, registry) {
840
971
  }
841
972
  catch (e) {
842
973
  if (e instanceof actions_1.ActionDiagnosticsError)
843
- return reply.code(422).send(e.diagnostics);
974
+ return reply.code(422).send(enrichDiag(e.diagnostics));
844
975
  throw e;
845
976
  }
846
977
  });
@@ -857,7 +988,7 @@ function registerActionRoutes(server, registry) {
857
988
  }
858
989
  catch (e) {
859
990
  if (e instanceof actions_1.ActionDiagnosticsError)
860
- return reply.code(422).send(e.diagnostics);
991
+ return reply.code(422).send(enrichDiag(e.diagnostics));
861
992
  throw e;
862
993
  }
863
994
  });
@@ -871,7 +1002,7 @@ function registerActionRoutes(server, registry) {
871
1002
  }
872
1003
  catch (e) {
873
1004
  if (e instanceof actions_1.ActionDiagnosticsError)
874
- return reply.code(422).send(e.diagnostics);
1005
+ return reply.code(422).send(enrichDiag(e.diagnostics));
875
1006
  throw e;
876
1007
  }
877
1008
  });
@@ -885,7 +1016,7 @@ function registerActionRoutes(server, registry) {
885
1016
  }
886
1017
  catch (e) {
887
1018
  if (e instanceof actions_1.ActionDiagnosticsError)
888
- return reply.code(422).send(e.diagnostics);
1019
+ return reply.code(422).send(enrichDiag(e.diagnostics));
889
1020
  throw e;
890
1021
  }
891
1022
  });
@@ -899,11 +1030,12 @@ function registerActionRoutes(server, registry) {
899
1030
  const { purpose, operator, ...opts } = req.body ?? {};
900
1031
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
901
1032
  try {
902
- return await Actions.scrollUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1033
+ const result = await Actions.scrollUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1034
+ return { ...result, session_id: s.id };
903
1035
  }
904
1036
  catch (e) {
905
1037
  if (e instanceof actions_1.ActionDiagnosticsError)
906
- return reply.code(422).send(e.diagnostics);
1038
+ return reply.code(422).send(enrichDiag(e.diagnostics));
907
1039
  throw e;
908
1040
  }
909
1041
  });
@@ -913,11 +1045,12 @@ function registerActionRoutes(server, registry) {
913
1045
  return;
914
1046
  const { purpose, operator, ...opts } = req.body;
915
1047
  try {
916
- return await Actions.loadMoreUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1048
+ const result = await Actions.loadMoreUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1049
+ return { ...result, session_id: s.id };
917
1050
  }
918
1051
  catch (e) {
919
1052
  if (e instanceof actions_1.ActionDiagnosticsError)
920
- return reply.code(422).send(e.diagnostics);
1053
+ return reply.code(422).send(enrichDiag(e.diagnostics));
921
1054
  throw e;
922
1055
  }
923
1056
  });
@@ -928,10 +1061,10 @@ function registerActionRoutes(server, registry) {
928
1061
  const s = resolve(req.params.id, reply);
929
1062
  if (!s)
930
1063
  return;
931
- const { scope, limit = 500, purpose, operator } = req.body ?? {};
1064
+ const { scope, limit = 500, include_unlabeled = false, purpose, operator } = req.body ?? {};
932
1065
  const bm = server.browserManager;
933
1066
  try {
934
- const elemResult = await Actions.elementMap(s.page, { scope, limit }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1067
+ const elemResult = await Actions.elementMap(s.page, { scope, limit, include_unlabeled }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
935
1068
  const snapshotId = 'snap_' + crypto_1.default.randomBytes(4).toString('hex');
936
1069
  const pageRev = bm?.getPageRev(s.id) ?? 0;
937
1070
  const elements = elemResult.elements.map((el) => ({ ...el, ref_id: `${snapshotId}:${el.element_id}` }));
@@ -940,9 +1073,229 @@ function registerActionRoutes(server, registry) {
940
1073
  }
941
1074
  catch (e) {
942
1075
  if (e instanceof actions_1.ActionDiagnosticsError)
943
- return reply.code(422).send(e.diagnostics);
1076
+ return reply.code(422).send(enrichDiag(e.diagnostics));
1077
+ throw e;
1078
+ }
1079
+ });
1080
+ // ---------------------------------------------------------------------------
1081
+ // R08-R10: Semantic element locator — find by role/text/label/placeholder/alt_text
1082
+ // ---------------------------------------------------------------------------
1083
+ server.post('/api/v1/sessions/:id/find', async (req, reply) => {
1084
+ const s = resolve(req.params.id, reply);
1085
+ if (!s)
1086
+ return;
1087
+ const { query_type, query, name, exact = false, nth = 0 } = req.body;
1088
+ try {
1089
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1090
+ let locator;
1091
+ if (query_type === 'role')
1092
+ locator = s.page.getByRole(query, { name, exact });
1093
+ else if (query_type === 'text')
1094
+ locator = s.page.getByText(query, { exact });
1095
+ else if (query_type === 'label')
1096
+ locator = s.page.getByLabel(query, { exact });
1097
+ else if (query_type === 'placeholder')
1098
+ locator = s.page.getByPlaceholder(query, { exact });
1099
+ else if (query_type === 'alt_text')
1100
+ locator = s.page.getByAltText(query, { exact });
1101
+ else
1102
+ return reply.code(400).send({ error: `Unknown query_type: ${query_type}`, valid: ['role', 'text', 'label', 'placeholder', 'alt_text'] });
1103
+ const count = await locator.count();
1104
+ if (count === 0)
1105
+ return { status: 'ok', found: false, count: 0, nth: 0, query_type, query };
1106
+ const target = locator.nth(nth);
1107
+ const bbox = await target.boundingBox().catch(() => null);
1108
+ const text = await target.textContent().catch(() => null);
1109
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1110
+ const tag = await target.evaluate((el) => el.tagName.toLowerCase()).catch(() => null);
1111
+ return { status: 'ok', found: true, count, nth, query_type, query, tag, text: text?.trim() || null, bbox };
1112
+ }
1113
+ catch (e) {
1114
+ if (e instanceof actions_1.ActionDiagnosticsError)
1115
+ return reply.code(422).send(enrichDiag(e.diagnostics));
1116
+ throw e;
1117
+ }
1118
+ });
1119
+ // ---------------------------------------------------------------------------
1120
+ // R08-R16: Upload URL — fetch remote URL and upload as file input
1121
+ // ---------------------------------------------------------------------------
1122
+ server.post('/api/v1/sessions/:id/upload_url', async (req, reply) => {
1123
+ const s = resolve(req.params.id, reply);
1124
+ if (!s)
1125
+ return;
1126
+ const { url, filename: filenameHint, mime_type, purpose, operator } = req.body;
1127
+ if (!url)
1128
+ return reply.code(400).send({ error: 'url is required' });
1129
+ const cssSelector = resolveTarget(req.body, reply, s.id);
1130
+ if (!cssSelector)
1131
+ return;
1132
+ let resp;
1133
+ try {
1134
+ resp = await fetch(url);
1135
+ }
1136
+ catch (e) {
1137
+ return reply.code(422).send({ error: 'fetch_failed', message: e.message, url });
1138
+ }
1139
+ if (!resp.ok)
1140
+ return reply.code(422).send({ error: 'fetch_failed', http_status: resp.status, url });
1141
+ const buf = Buffer.from(await resp.arrayBuffer());
1142
+ const filename = filenameHint ?? url.split('/').pop()?.split('?')[0] ?? 'file';
1143
+ const ext = path_1.default.extname(filename) || '.bin';
1144
+ const tmpPath = path_1.default.join(os_1.default.tmpdir(), `agentmb_${crypto_1.default.randomBytes(4).toString('hex')}${ext}`);
1145
+ try {
1146
+ await fs_1.default.promises.writeFile(tmpPath, buf);
1147
+ const b64 = buf.toString('base64');
1148
+ const result = await Actions.uploadFile(s.page, cssSelector, b64, filename, mime_type, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1149
+ return { ...result, url, fetched_bytes: buf.length };
1150
+ }
1151
+ catch (e) {
1152
+ if (e instanceof actions_1.ActionDiagnosticsError)
1153
+ return reply.code(422).send(enrichDiag(e.diagnostics));
944
1154
  throw e;
945
1155
  }
1156
+ finally {
1157
+ fs_1.default.promises.unlink(tmpPath).catch(() => { });
1158
+ }
1159
+ });
1160
+ // ---------------------------------------------------------------------------
1161
+ // R08-R18: run_steps — batch action dispatcher
1162
+ // Supported actions: navigate, click, fill, type, press, hover, scroll,
1163
+ // wait_for_selector, wait_text, screenshot, eval
1164
+ // r08-c07: resolveRefIdForStep — throws (instead of reply) for use inside step loops
1165
+ // ---------------------------------------------------------------------------
1166
+ function resolveRefIdForStep(params, sessionId) {
1167
+ if (params.ref_id) {
1168
+ const bm = server.browserManager;
1169
+ if (!bm)
1170
+ throw new Error('ref_id resolution requires BrowserManager');
1171
+ const colonIdx = params.ref_id.lastIndexOf(':');
1172
+ if (colonIdx === -1)
1173
+ throw new Error(`Invalid ref_id format: "${params.ref_id}"; expected "snap_XXXXXX:eN"`);
1174
+ const snapshotId = params.ref_id.slice(0, colonIdx);
1175
+ const eid = params.ref_id.slice(colonIdx + 1);
1176
+ const snapshot = bm.getSnapshot(sessionId, snapshotId);
1177
+ if (!snapshot)
1178
+ throw new Error(`stale_ref: snapshot "${snapshotId}" not found or expired; call snapshot_map again`);
1179
+ const currentRev = bm.getPageRev(sessionId);
1180
+ if (snapshot.page_rev !== currentRev)
1181
+ throw new Error(`stale_ref: page changed (snapshot_rev=${snapshot.page_rev}, current=${currentRev}); call snapshot_map again`);
1182
+ return `[data-agentmb-eid="${eid}"]`;
1183
+ }
1184
+ if (params.element_id)
1185
+ return `[data-agentmb-eid="${params.element_id}"]`;
1186
+ return params.selector;
1187
+ }
1188
+ server.post('/api/v1/sessions/:id/run_steps', async (req, reply) => {
1189
+ const s = resolve(req.params.id, reply);
1190
+ if (!s)
1191
+ return;
1192
+ const { steps, stop_on_error = true, purpose, operator } = req.body;
1193
+ if (!Array.isArray(steps) || steps.length === 0)
1194
+ return reply.code(400).send({ error: 'steps must be a non-empty array' });
1195
+ if (steps.length > 100)
1196
+ return reply.code(400).send({ error: 'steps must not exceed 100' });
1197
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1198
+ const results = [];
1199
+ const op = inferOperator(req, s, operator);
1200
+ for (let i = 0; i < steps.length; i++) {
1201
+ const step = steps[i];
1202
+ const { action, params = {} } = step;
1203
+ const stepPurpose = params.purpose ?? purpose;
1204
+ try {
1205
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1206
+ let result;
1207
+ switch (action) {
1208
+ case 'navigate':
1209
+ result = await Actions.navigate(s.page, params.url, params.wait_until ?? 'load', getLogger(), s.id, stepPurpose, op);
1210
+ break;
1211
+ case 'click': {
1212
+ if (!params.selector && !params.element_id && !params.ref_id)
1213
+ throw new Error('click requires selector, element_id, or ref_id');
1214
+ const sel = resolveRefIdForStep(params, s.id);
1215
+ result = await Actions.click(s.page, sel, params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
1216
+ break;
1217
+ }
1218
+ case 'fill': {
1219
+ if (!params.selector && !params.element_id && !params.ref_id)
1220
+ throw new Error('fill requires selector, element_id, or ref_id');
1221
+ const sel = resolveRefIdForStep(params, s.id);
1222
+ result = await Actions.fill(s.page, sel, params.value ?? '', getLogger(), s.id, stepPurpose, op);
1223
+ break;
1224
+ }
1225
+ case 'type': {
1226
+ if (!params.selector && !params.element_id && !params.ref_id)
1227
+ throw new Error('type requires selector, element_id, or ref_id');
1228
+ const sel = resolveRefIdForStep(params, s.id);
1229
+ result = await Actions.typeText(s.page, sel, params.text ?? '', params.delay_ms ?? 0, getLogger(), s.id, stepPurpose, op);
1230
+ break;
1231
+ }
1232
+ case 'press': {
1233
+ if (!params.selector && !params.element_id && !params.ref_id)
1234
+ throw new Error('press requires selector, element_id, or ref_id');
1235
+ const sel = resolveRefIdForStep(params, s.id);
1236
+ result = await Actions.press(s.page, sel, params.key ?? '', getLogger(), s.id, stepPurpose, op);
1237
+ break;
1238
+ }
1239
+ case 'hover': {
1240
+ if (!params.selector && !params.element_id && !params.ref_id)
1241
+ throw new Error('hover requires selector, element_id, or ref_id');
1242
+ const sel = resolveRefIdForStep(params, s.id);
1243
+ result = await Actions.hover(s.page, sel, getLogger(), s.id, stepPurpose, op);
1244
+ break;
1245
+ }
1246
+ case 'scroll': {
1247
+ if (!params.selector && !params.element_id && !params.ref_id)
1248
+ throw new Error('scroll requires selector, element_id, or ref_id');
1249
+ const sel = resolveRefIdForStep(params, s.id);
1250
+ result = await Actions.scroll(s.page, sel, { delta_x: params.delta_x ?? 0, delta_y: params.delta_y ?? 300 }, getLogger(), s.id, stepPurpose, op);
1251
+ break;
1252
+ }
1253
+ case 'wait_for_selector':
1254
+ result = await Actions.waitForSelector(s.page, params.selector, params.state ?? 'visible', params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
1255
+ break;
1256
+ case 'wait_text':
1257
+ result = await Actions.waitForText(s.page, params.text, params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
1258
+ break;
1259
+ case 'screenshot':
1260
+ result = await Actions.screenshot(s.page, params.format ?? 'png', params.full_page ?? false, getLogger(), s.id, stepPurpose, op);
1261
+ break;
1262
+ case 'eval':
1263
+ result = await Actions.evaluate(s.page, params.expression, getLogger(), s.id, stepPurpose, op);
1264
+ break;
1265
+ default:
1266
+ throw new Error(`unsupported action: ${action}`);
1267
+ }
1268
+ results.push({ step: i + 1, action, result });
1269
+ }
1270
+ catch (e) {
1271
+ const errPayload = e instanceof actions_1.ActionDiagnosticsError
1272
+ ? enrichDiag(e.diagnostics)
1273
+ : { error: e.message ?? String(e) };
1274
+ results.push({ step: i + 1, action, error: errPayload });
1275
+ if (stop_on_error)
1276
+ break;
1277
+ }
1278
+ }
1279
+ const failed = results.filter(r => r.error);
1280
+ return {
1281
+ status: failed.length === 0 ? 'ok' : (results.filter(r => r.result).length > 0 ? 'partial' : 'failed'),
1282
+ total_steps: steps.length,
1283
+ completed_steps: results.filter(r => r.result).length,
1284
+ failed_steps: failed.length,
1285
+ results,
1286
+ };
1287
+ });
1288
+ // ---------------------------------------------------------------------------
1289
+ // R08-R12: Snapshot Ref 强化 — GET page_rev endpoint
1290
+ // Lets clients cheaply check if the page has changed since last snapshot.
1291
+ // ---------------------------------------------------------------------------
1292
+ server.get('/api/v1/sessions/:id/page_rev', async (req, reply) => {
1293
+ const s = resolve(req.params.id, reply);
1294
+ if (!s)
1295
+ return;
1296
+ const bm = server.browserManager;
1297
+ const page_rev = bm?.getPageRev(s.id) ?? 0;
1298
+ return { status: 'ok', session_id: req.params.id, page_rev, url: s.page.url() };
946
1299
  });
947
1300
  }
948
1301
  //# sourceMappingURL=actions.js.map