agentmb 0.1.1 → 0.3.2

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 (50) hide show
  1. package/README.md +1002 -153
  2. package/dist/browser/actions.d.ts +25 -2
  3. package/dist/browser/actions.d.ts.map +1 -1
  4. package/dist/browser/actions.js +122 -22
  5. package/dist/browser/actions.js.map +1 -1
  6. package/dist/browser/manager.d.ts +35 -0
  7. package/dist/browser/manager.d.ts.map +1 -1
  8. package/dist/browser/manager.js +244 -16
  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 +342 -80
  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 +76 -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 +516 -78
  26. package/dist/daemon/routes/actions.js.map +1 -1
  27. package/dist/daemon/routes/interaction.d.ts.map +1 -1
  28. package/dist/daemon/routes/interaction.js +10 -1
  29. package/dist/daemon/routes/interaction.js.map +1 -1
  30. package/dist/daemon/routes/sessions.d.ts.map +1 -1
  31. package/dist/daemon/routes/sessions.js +314 -3
  32. package/dist/daemon/routes/sessions.js.map +1 -1
  33. package/dist/daemon/routes/state.d.ts.map +1 -1
  34. package/dist/daemon/routes/state.js +26 -0
  35. package/dist/daemon/routes/state.js.map +1 -1
  36. package/dist/daemon/server.js +1 -1
  37. package/dist/daemon/session.d.ts +19 -0
  38. package/dist/daemon/session.d.ts.map +1 -1
  39. package/dist/daemon/session.js +13 -0
  40. package/dist/daemon/session.js.map +1 -1
  41. package/dist/policy/types.d.ts.map +1 -1
  42. package/dist/policy/types.js +14 -12
  43. package/dist/policy/types.js.map +1 -1
  44. package/package.json +4 -2
  45. package/skills/agentmb/SKILL.md +541 -0
  46. package/skills/agentmb/references/authentication.md +180 -0
  47. package/skills/agentmb/references/browser-modes.md +167 -0
  48. package/skills/agentmb/references/commands.md +231 -0
  49. package/skills/agentmb/references/locator-modes.md +254 -0
  50. package/skills/agentmb/references/session-management.md +260 -0
@@ -38,10 +38,48 @@ 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"));
44
+ const url_1 = require("url");
41
45
  require("../types"); // T11: Fastify type augmentation
42
46
  const Actions = __importStar(require("../../browser/actions"));
43
47
  const actions_1 = require("../../browser/actions");
44
48
  const engine_1 = require("../../policy/engine");
49
+ // ---------------------------------------------------------------------------
50
+ // R09-C04-T12: Sensitive domain detection
51
+ // ---------------------------------------------------------------------------
52
+ const SENSITIVE_DOMAIN_PATTERNS = [
53
+ { re: /bank|banking|finance|financial|payment|paypal|stripe/i, category: 'financial' },
54
+ { re: /medical|health|hospital|clinic|pharma/i, category: 'medical' },
55
+ { re: /gambling|casino|betting|poker/i, category: 'gambling' },
56
+ { re: /adult|porn|nsfw|xxx/i, category: 'adult' },
57
+ { re: /crypto|bitcoin|wallet|exchange/i, category: 'crypto' },
58
+ ];
59
+ // Append extra patterns from env (comma-separated regexes)
60
+ const _envPatterns = process.env.AGENTMB_SENSITIVE_DOMAINS;
61
+ if (_envPatterns) {
62
+ for (const p of _envPatterns.split(',').map(s => s.trim()).filter(Boolean)) {
63
+ try {
64
+ SENSITIVE_DOMAIN_PATTERNS.push({ re: new RegExp(p, 'i'), category: 'custom' });
65
+ }
66
+ catch { /* ignore invalid regex */ }
67
+ }
68
+ }
69
+ function detectSensitiveDomain(url) {
70
+ let domain = '';
71
+ try {
72
+ domain = new URL(url).hostname;
73
+ }
74
+ catch {
75
+ return { sensitive: false };
76
+ }
77
+ for (const { re, category } of SENSITIVE_DOMAIN_PATTERNS) {
78
+ if (re.test(domain))
79
+ return { sensitive: true, category, domain };
80
+ }
81
+ return { sensitive: false };
82
+ }
45
83
  /** Structured error when a frame selector matches no frame in the page. */
46
84
  class FrameResolutionError extends Error {
47
85
  selector;
@@ -146,6 +184,59 @@ async function applyPolicy(server, sessionId, domain, action, opts, reply) {
146
184
  }
147
185
  return true;
148
186
  }
187
+ function pfRange(field, value, min, max) {
188
+ if (value === undefined)
189
+ return null;
190
+ return value < min || value > max ? { field, constraint: `must be ${min}–${max}`, value } : null;
191
+ }
192
+ function pfMaxLen(field, value, max) {
193
+ if (!value)
194
+ return null;
195
+ return value.length > max ? { field, constraint: `max length ${max} chars`, value: value.length } : null;
196
+ }
197
+ /** Run an ordered list of checks; sends 400 + returns false on first violation. */
198
+ function preflight(checks, reply) {
199
+ const v = checks.find(Boolean);
200
+ if (v) {
201
+ reply.code(400).send({ error: 'preflight_failed', field: v.field, constraint: v.constraint, value: v.value });
202
+ return false;
203
+ }
204
+ return true;
205
+ }
206
+ async function applyStabilityPre(page, opts) {
207
+ if (!opts)
208
+ return;
209
+ if (opts.wait_before_ms)
210
+ await new Promise(r => setTimeout(r, opts.wait_before_ms));
211
+ if (opts.wait_dom_stable_ms) {
212
+ try {
213
+ await page.waitForFunction('document.readyState === "complete"', undefined, { timeout: opts.wait_dom_stable_ms });
214
+ }
215
+ catch { /* timeout is acceptable */ }
216
+ }
217
+ }
218
+ async function applyStabilityPost(page, opts) {
219
+ if (!opts)
220
+ return;
221
+ if (opts.wait_after_ms)
222
+ await new Promise(r => setTimeout(r, opts.wait_after_ms));
223
+ }
224
+ // ---------------------------------------------------------------------------
225
+ // R08-R13: Error recovery hints — enrich 422 ActionDiagnostics with hint
226
+ // ---------------------------------------------------------------------------
227
+ function enrichDiag(diag) {
228
+ const msg = diag.error.toLowerCase();
229
+ let recovery_hint;
230
+ if (msg.includes('timeout') || msg.includes('waiting for'))
231
+ recovery_hint = 'Increase timeout_ms or add stability.wait_before_ms; ensure element is visible before acting';
232
+ else if (msg.includes('target closed') || msg.includes('execution context') || msg.includes('detached'))
233
+ recovery_hint = 'Page may have navigated or element was removed; re-navigate or re-snapshot';
234
+ else if (msg.includes('not found') || msg.includes('no element') || msg.includes('failed to find'))
235
+ recovery_hint = 'Check selector; use snapshot_map to verify element exists on current page';
236
+ else if (msg.includes('intercept') || msg.includes('overlap') || msg.includes('obscur'))
237
+ recovery_hint = 'Element may be covered by overlay; try executor=auto_fallback or scroll into view first';
238
+ return recovery_hint ? { ...diag, recovery_hint } : diag;
239
+ }
149
240
  function registerActionRoutes(server, registry) {
150
241
  function getLogger() {
151
242
  return server.auditLogger;
@@ -172,7 +263,11 @@ function registerActionRoutes(server, registry) {
172
263
  const eid = input.ref_id.slice(colonIdx + 1);
173
264
  const snapshot = bm.getSnapshot(sessionId, snapshotId);
174
265
  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' });
266
+ reply.code(409).send({
267
+ error: 'stale_ref', ref_id: input.ref_id,
268
+ message: 'Snapshot not found or expired; call snapshot_map again',
269
+ suggestions: ['call snapshot_map to get fresh ref_ids', 'use selector or element_id as fallback'],
270
+ });
176
271
  return null;
177
272
  }
178
273
  const currentRev = bm.getPageRev(sessionId);
@@ -183,6 +278,7 @@ function registerActionRoutes(server, registry) {
183
278
  snapshot_page_rev: snapshot.page_rev,
184
279
  current_page_rev: currentRev,
185
280
  message: 'Page has changed since snapshot was taken; call snapshot_map again',
281
+ suggestions: ['call snapshot_map to get fresh ref_ids', 'if page is stable, the navigation event incremented page_rev — wait and resnapshot'],
186
282
  });
187
283
  return null;
188
284
  }
@@ -213,24 +309,78 @@ function registerActionRoutes(server, registry) {
213
309
  }
214
310
  return result;
215
311
  }
312
+ /** Resolve a live session with optional page_id override (R09-C03 multi-page targeting). */
313
+ function resolveWithPage(id, pageId, reply) {
314
+ const s = resolve(id, reply);
315
+ if (!s)
316
+ return null;
317
+ if (!pageId)
318
+ return s;
319
+ const bm = server.browserManager;
320
+ const page = bm?.getPageById(id, pageId);
321
+ if (!page) {
322
+ reply.code(404).send({ error: `Page ${pageId} not found in session ${id}. Use GET /api/v1/sessions/${id}/pages to list available pages.` });
323
+ return null;
324
+ }
325
+ return { ...s, page };
326
+ }
216
327
  // POST /api/v1/sessions/:id/navigate
217
328
  server.post('/api/v1/sessions/:id/navigate', async (req, reply) => {
218
- const s = resolve(req.params.id, reply);
329
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
219
330
  if (!s)
220
331
  return;
221
- const { url, wait_until = 'load', purpose, operator, sensitive, retry } = req.body;
332
+ const { url, wait_until = 'load', timeout_ms = 30_000, purpose, operator, sensitive, retry } = req.body;
333
+ // R09-C07-P0: validate timeout_ms to prevent abuse (0 disables, max 60 s)
334
+ if (!preflight([pfRange('timeout_ms', timeout_ms, 0, 60_000)], reply))
335
+ return;
336
+ // R09-C06-P1: file:// URL guard — require allow_dirs whitelist
337
+ // R09-C07-P0: use fs.realpath to resolve symlinks before whitelist check (symlink traversal fix)
338
+ if (url.startsWith('file://')) {
339
+ const bm = server.browserManager;
340
+ const allowDirs = bm?.getAllowDirs(req.params.id) ?? [];
341
+ if (allowDirs.length === 0) {
342
+ return reply.code(403).send({ error: 'file:// navigation requires allow_dirs on the session. Set allow_dirs when creating session.' });
343
+ }
344
+ let filePath;
345
+ try {
346
+ filePath = (0, url_1.fileURLToPath)(url);
347
+ }
348
+ catch {
349
+ return reply.code(400).send({ error: 'Invalid file:// URL format' });
350
+ }
351
+ let abs;
352
+ try {
353
+ abs = await fs_1.default.promises.realpath(filePath);
354
+ }
355
+ catch {
356
+ return reply.code(404).send({ error: `file:// path does not exist: ${filePath}` });
357
+ }
358
+ const allowed = allowDirs.some(d => abs === d || abs.startsWith(d + path_1.default.sep));
359
+ if (!allowed) {
360
+ return reply.code(403).send({ error: `file:// path ${abs} is not within allowed directories.` });
361
+ }
362
+ }
222
363
  const domain = (0, engine_1.extractDomain)(url);
223
364
  if (!await applyPolicy(server, req.params.id, domain, 'navigate', { sensitive, retry }, reply))
224
365
  return;
225
- return Actions.navigate(s.page, url, wait_until, getLogger(), s.id, purpose, inferOperator(req, s, operator));
366
+ const navResult = await Actions.navigate(s.page, url, wait_until, getLogger(), s.id, purpose, inferOperator(req, s, operator), timeout_ms);
367
+ const sensitiveInfo = detectSensitiveDomain(url);
368
+ const sensitive_warning = sensitiveInfo.sensitive
369
+ ? { domain: sensitiveInfo.domain, category: sensitiveInfo.category, message: `Navigating to potentially sensitive domain: ${sensitiveInfo.domain}` }
370
+ : undefined;
371
+ return { ...navResult, ...(sensitive_warning ? { sensitive_warning } : {}) };
226
372
  });
227
373
  // POST /api/v1/sessions/:id/click
228
- // T21: add fallback_x / fallback_y if DOM click fails, retry via page.mouse.click()
374
+ // R08-R06: executor='auto_fallback' automatically retries via bbox coords on DOM failure
375
+ // R08-R02: stability.wait_before_ms / wait_after_ms / wait_dom_stable_ms
376
+ // R08-R09: preflight validates timeout_ms range
229
377
  server.post('/api/v1/sessions/:id/click', async (req, reply) => {
230
- const s = resolve(req.params.id, reply);
378
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
231
379
  if (!s)
232
380
  return;
233
- const { timeout_ms = 5000, frame, purpose, operator, sensitive, retry, fallback_x, fallback_y } = req.body;
381
+ const { timeout_ms = 5000, frame, purpose, operator, sensitive, retry, fallback_x, fallback_y, executor = 'strict', stability } = req.body;
382
+ if (!preflight([pfRange('timeout_ms', timeout_ms, 50, 60000)], reply))
383
+ return;
234
384
  const selector = resolveTarget(req.body, reply, s.id);
235
385
  if (!selector)
236
386
  return;
@@ -239,32 +389,63 @@ function registerActionRoutes(server, registry) {
239
389
  const target = resolveOrReply(s.page, frame, reply);
240
390
  if (!target)
241
391
  return;
392
+ await applyStabilityPre(s.page, stability);
242
393
  try {
243
- return await Actions.click(target, selector, timeout_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator));
394
+ const result = await Actions.click(target, selector, timeout_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator));
395
+ await applyStabilityPost(s.page, stability);
396
+ return { ...result, executed_via: 'high_level' };
244
397
  }
245
398
  catch (domErr) {
246
- // T21 dual-track: if fallback coordinates provided, retry via mouse.click()
247
- if (fallback_x !== undefined && fallback_y !== undefined && s.page) {
399
+ // auto_fallback: resolve element bbox and retry via mouse.click()
400
+ if (executor === 'auto_fallback') {
248
401
  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 };
402
+ const bbox = await target.locator(selector).boundingBox();
403
+ if (bbox) {
404
+ let cx = Math.round(bbox.x + bbox.width / 2);
405
+ let cy = Math.round(bbox.y + bbox.height / 2);
406
+ // Frame offset compensation: when target is a Frame (not the Page),
407
+ // boundingBox() coords are relative to the frame viewport; add the
408
+ // frame element's page-level offset so mouse.click lands correctly.
409
+ if (frame && target !== s.page) {
410
+ try {
411
+ 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}; })()');
412
+ cx += Math.round(frameRect.x);
413
+ cy += Math.round(frameRect.y);
414
+ }
415
+ catch { /* best-effort; proceed with uncompensated coords */ }
416
+ }
417
+ await s.page.mouse.click(cx, cy);
418
+ await applyStabilityPost(s.page, stability);
419
+ return { status: 'ok', selector, executed_via: 'low_level', fallback_x: cx, fallback_y: cy, duration_ms: 0 };
420
+ }
252
421
  }
253
- catch (coordErr) {
254
- // Both tracks failed — report original DOM error
422
+ catch { /* fallback also failed — fall through to original error */ }
423
+ }
424
+ // explicit fallback coordinates (legacy T21 path)
425
+ if (fallback_x !== undefined && fallback_y !== undefined) {
426
+ try {
427
+ await s.page.mouse.click(fallback_x, fallback_y);
428
+ return { status: 'ok', selector, executed_via: 'low_level', track: 'coords', fallback_x, fallback_y, duration_ms: 0 };
255
429
  }
430
+ catch { /* both tracks failed */ }
431
+ }
432
+ if (domErr instanceof Actions.ActionDiagnosticsError) {
433
+ const diag = enrichDiag({ ...domErr.diagnostics, suggested_fallback: 'retry with executor="auto_fallback" to attempt coordinates-based click' });
434
+ return reply.code(422).send(diag);
256
435
  }
257
- if (domErr instanceof Actions.ActionDiagnosticsError)
258
- return reply.code(422).send(domErr.diagnostics);
259
436
  throw domErr;
260
437
  }
261
438
  });
262
439
  // POST /api/v1/sessions/:id/fill
440
+ // R08-R01: fill_strategy='type' uses pressSequentially with char_delay_ms (humanization)
441
+ // R08-R02: stability options; R08-R09: preflight value length
263
442
  server.post('/api/v1/sessions/:id/fill', async (req, reply) => {
264
- const s = resolve(req.params.id, reply);
443
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
265
444
  if (!s)
266
445
  return;
267
- const { value, frame, purpose, operator, sensitive, retry } = req.body;
446
+ const { value, frame, purpose, operator, sensitive, retry, stability, fill_strategy = 'instant', char_delay_ms = 0 } = req.body;
447
+ if (!preflight([pfMaxLen('value', value, 100_000)], reply))
448
+ return;
268
449
  const selector = resolveTarget(req.body, reply, s.id);
269
450
  if (!selector)
270
451
  return;
@@ -273,11 +454,16 @@ function registerActionRoutes(server, registry) {
273
454
  const target = resolveOrReply(s.page, frame, reply);
274
455
  if (!target)
275
456
  return;
276
- return Actions.fill(target, selector, value, getLogger(), s.id, purpose, inferOperator(req, s, operator));
457
+ await applyStabilityPre(s.page, stability);
458
+ const result = fill_strategy === 'type'
459
+ ? await Actions.typeText(target, selector, value, char_delay_ms, getLogger(), s.id, purpose, inferOperator(req, s, operator))
460
+ : await Actions.fill(target, selector, value, getLogger(), s.id, purpose, inferOperator(req, s, operator));
461
+ await applyStabilityPost(s.page, stability);
462
+ return result;
277
463
  });
278
464
  // POST /api/v1/sessions/:id/eval
279
465
  server.post('/api/v1/sessions/:id/eval', async (req, reply) => {
280
- const s = resolve(req.params.id, reply);
466
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
281
467
  if (!s)
282
468
  return;
283
469
  const { expression, frame, purpose, operator, sensitive, retry } = req.body;
@@ -291,7 +477,7 @@ function registerActionRoutes(server, registry) {
291
477
  }
292
478
  catch (e) {
293
479
  if (e instanceof actions_1.ActionDiagnosticsError)
294
- return reply.code(422).send(e.diagnostics);
480
+ return reply.code(422).send(enrichDiag(e.diagnostics));
295
481
  throw e;
296
482
  }
297
483
  });
@@ -311,13 +497,13 @@ function registerActionRoutes(server, registry) {
311
497
  }
312
498
  catch (e) {
313
499
  if (e instanceof actions_1.ActionDiagnosticsError)
314
- return reply.code(422).send(e.diagnostics);
500
+ return reply.code(422).send(enrichDiag(e.diagnostics));
315
501
  throw e;
316
502
  }
317
503
  });
318
504
  // POST /api/v1/sessions/:id/screenshot
319
505
  server.post('/api/v1/sessions/:id/screenshot', async (req, reply) => {
320
- const s = resolve(req.params.id, reply);
506
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
321
507
  if (!s)
322
508
  return;
323
509
  const { format = 'png', full_page = false, purpose, operator } = req.body ?? {};
@@ -326,13 +512,13 @@ function registerActionRoutes(server, registry) {
326
512
  }
327
513
  catch (e) {
328
514
  if (e instanceof actions_1.ActionDiagnosticsError)
329
- return reply.code(422).send(e.diagnostics);
515
+ return reply.code(422).send(enrichDiag(e.diagnostics));
330
516
  throw e;
331
517
  }
332
518
  });
333
519
  // POST /api/v1/sessions/:id/type
334
520
  server.post('/api/v1/sessions/:id/type', async (req, reply) => {
335
- const s = resolve(req.params.id, reply);
521
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
336
522
  if (!s)
337
523
  return;
338
524
  const { text, delay_ms = 0, frame, purpose, operator, sensitive, retry } = req.body;
@@ -350,13 +536,13 @@ function registerActionRoutes(server, registry) {
350
536
  }
351
537
  catch (e) {
352
538
  if (e instanceof actions_1.ActionDiagnosticsError)
353
- return reply.code(422).send(e.diagnostics);
539
+ return reply.code(422).send(enrichDiag(e.diagnostics));
354
540
  throw e;
355
541
  }
356
542
  });
357
543
  // POST /api/v1/sessions/:id/press
358
544
  server.post('/api/v1/sessions/:id/press', async (req, reply) => {
359
- const s = resolve(req.params.id, reply);
545
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
360
546
  if (!s)
361
547
  return;
362
548
  const { key, frame, purpose, operator, sensitive, retry } = req.body;
@@ -373,7 +559,7 @@ function registerActionRoutes(server, registry) {
373
559
  }
374
560
  catch (e) {
375
561
  if (e instanceof actions_1.ActionDiagnosticsError)
376
- return reply.code(422).send(e.diagnostics);
562
+ return reply.code(422).send(enrichDiag(e.diagnostics));
377
563
  throw e;
378
564
  }
379
565
  });
@@ -391,7 +577,7 @@ function registerActionRoutes(server, registry) {
391
577
  }
392
578
  catch (e) {
393
579
  if (e instanceof actions_1.ActionDiagnosticsError)
394
- return reply.code(422).send(e.diagnostics);
580
+ return reply.code(422).send(enrichDiag(e.diagnostics));
395
581
  throw e;
396
582
  }
397
583
  });
@@ -412,7 +598,7 @@ function registerActionRoutes(server, registry) {
412
598
  }
413
599
  catch (e) {
414
600
  if (e instanceof actions_1.ActionDiagnosticsError)
415
- return reply.code(422).send(e.diagnostics);
601
+ return reply.code(422).send(enrichDiag(e.diagnostics));
416
602
  throw e;
417
603
  }
418
604
  });
@@ -430,7 +616,7 @@ function registerActionRoutes(server, registry) {
430
616
  }
431
617
  catch (e) {
432
618
  if (e instanceof actions_1.ActionDiagnosticsError)
433
- return reply.code(422).send(e.diagnostics);
619
+ return reply.code(422).send(enrichDiag(e.diagnostics));
434
620
  throw e;
435
621
  }
436
622
  });
@@ -445,7 +631,7 @@ function registerActionRoutes(server, registry) {
445
631
  }
446
632
  catch (e) {
447
633
  if (e instanceof actions_1.ActionDiagnosticsError)
448
- return reply.code(422).send(e.diagnostics);
634
+ return reply.code(422).send(enrichDiag(e.diagnostics));
449
635
  throw e;
450
636
  }
451
637
  });
@@ -461,7 +647,7 @@ function registerActionRoutes(server, registry) {
461
647
  }
462
648
  catch (e) {
463
649
  if (e instanceof actions_1.ActionDiagnosticsError)
464
- return reply.code(422).send(e.diagnostics);
650
+ return reply.code(422).send(enrichDiag(e.diagnostics));
465
651
  throw e;
466
652
  }
467
653
  });
@@ -481,22 +667,35 @@ function registerActionRoutes(server, registry) {
481
667
  }
482
668
  catch (e) {
483
669
  if (e instanceof actions_1.ActionDiagnosticsError)
484
- return reply.code(422).send(e.diagnostics);
670
+ return reply.code(422).send(enrichDiag(e.diagnostics));
485
671
  throw e;
486
672
  }
487
673
  });
488
674
  // POST /api/v1/sessions/:id/download
675
+ // T07: guard on accept_downloads; T08: element_id / ref_id support
489
676
  server.post('/api/v1/sessions/:id/download', async (req, reply) => {
490
677
  const s = resolve(req.params.id, reply);
491
678
  if (!s)
492
679
  return;
493
- const { selector, timeout_ms = 30000, max_bytes = 50 * 1024 * 1024, purpose, operator } = req.body;
680
+ // T07: check accept_downloads is enabled
681
+ const bm = server.browserManager;
682
+ if (bm && !bm.getAcceptDownloads(s.id)) {
683
+ return reply.code(422).send({
684
+ error: 'download_not_enabled',
685
+ message: 'Downloads are disabled for this session. Create the session with accept_downloads=true (CLI: agentmb session new --accept-downloads).',
686
+ });
687
+ }
688
+ const { timeout_ms = 30000, max_bytes = 50 * 1024 * 1024, purpose, operator } = req.body;
689
+ // T08: resolve selector/element_id/ref_id to CSS selector
690
+ const selector = resolveTarget(req.body, reply, s.id);
691
+ if (!selector)
692
+ return;
494
693
  try {
495
694
  return await Actions.downloadFile(s.page, selector, timeout_ms, max_bytes, getLogger(), s.id, purpose, inferOperator(req, s, operator));
496
695
  }
497
696
  catch (e) {
498
697
  if (e instanceof actions_1.ActionDiagnosticsError)
499
- return reply.code(422).send(e.diagnostics);
698
+ return reply.code(422).send(enrichDiag(e.diagnostics));
500
699
  throw e;
501
700
  }
502
701
  });
@@ -513,16 +712,16 @@ function registerActionRoutes(server, registry) {
513
712
  // R07-T01: element_map — scan page, assign stable element IDs
514
713
  // ---------------------------------------------------------------------------
515
714
  server.post('/api/v1/sessions/:id/element_map', async (req, reply) => {
516
- const s = resolve(req.params.id, reply);
715
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
517
716
  if (!s)
518
717
  return;
519
- const { scope, limit = 500, purpose, operator } = req.body ?? {};
718
+ const { scope, limit = 500, include_unlabeled = false, purpose, operator } = req.body ?? {};
520
719
  try {
521
- return await Actions.elementMap(s.page, { scope, limit }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
720
+ return await Actions.elementMap(s.page, { scope, limit, include_unlabeled }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
522
721
  }
523
722
  catch (e) {
524
723
  if (e instanceof actions_1.ActionDiagnosticsError)
525
- return reply.code(422).send(e.diagnostics);
724
+ return reply.code(422).send(enrichDiag(e.diagnostics));
526
725
  throw e;
527
726
  }
528
727
  });
@@ -547,7 +746,7 @@ function registerActionRoutes(server, registry) {
547
746
  }
548
747
  catch (e) {
549
748
  if (e instanceof actions_1.ActionDiagnosticsError)
550
- return reply.code(422).send(e.diagnostics);
749
+ return reply.code(422).send(enrichDiag(e.diagnostics));
551
750
  throw e;
552
751
  }
553
752
  });
@@ -572,7 +771,7 @@ function registerActionRoutes(server, registry) {
572
771
  }
573
772
  catch (e) {
574
773
  if (e instanceof actions_1.ActionDiagnosticsError)
575
- return reply.code(422).send(e.diagnostics);
774
+ return reply.code(422).send(enrichDiag(e.diagnostics));
576
775
  throw e;
577
776
  }
578
777
  });
@@ -589,7 +788,7 @@ function registerActionRoutes(server, registry) {
589
788
  }
590
789
  catch (e) {
591
790
  if (e instanceof actions_1.ActionDiagnosticsError)
592
- return reply.code(422).send(e.diagnostics);
791
+ return reply.code(422).send(enrichDiag(e.diagnostics));
593
792
  throw e;
594
793
  }
595
794
  });
@@ -611,7 +810,7 @@ function registerActionRoutes(server, registry) {
611
810
  }
612
811
  catch (e) {
613
812
  if (e instanceof actions_1.ActionDiagnosticsError)
614
- return reply.code(422).send(e.diagnostics);
813
+ return reply.code(422).send(enrichDiag(e.diagnostics));
615
814
  throw e;
616
815
  }
617
816
  });
@@ -630,7 +829,7 @@ function registerActionRoutes(server, registry) {
630
829
  }
631
830
  catch (e) {
632
831
  if (e instanceof actions_1.ActionDiagnosticsError)
633
- return reply.code(422).send(e.diagnostics);
832
+ return reply.code(422).send(enrichDiag(e.diagnostics));
634
833
  throw e;
635
834
  }
636
835
  });
@@ -649,7 +848,7 @@ function registerActionRoutes(server, registry) {
649
848
  }
650
849
  catch (e) {
651
850
  if (e instanceof actions_1.ActionDiagnosticsError)
652
- return reply.code(422).send(e.diagnostics);
851
+ return reply.code(422).send(enrichDiag(e.diagnostics));
653
852
  throw e;
654
853
  }
655
854
  });
@@ -668,12 +867,12 @@ function registerActionRoutes(server, registry) {
668
867
  }
669
868
  catch (e) {
670
869
  if (e instanceof actions_1.ActionDiagnosticsError)
671
- return reply.code(422).send(e.diagnostics);
870
+ return reply.code(422).send(enrichDiag(e.diagnostics));
672
871
  throw e;
673
872
  }
674
873
  });
675
874
  server.post('/api/v1/sessions/:id/scroll', async (req, reply) => {
676
- const s = resolve(req.params.id, reply);
875
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
677
876
  if (!s)
678
877
  return;
679
878
  const selector = resolveTarget(req.body, reply, s.id);
@@ -688,7 +887,7 @@ function registerActionRoutes(server, registry) {
688
887
  }
689
888
  catch (e) {
690
889
  if (e instanceof actions_1.ActionDiagnosticsError)
691
- return reply.code(422).send(e.diagnostics);
890
+ return reply.code(422).send(enrichDiag(e.diagnostics));
692
891
  throw e;
693
892
  }
694
893
  });
@@ -707,7 +906,7 @@ function registerActionRoutes(server, registry) {
707
906
  }
708
907
  catch (e) {
709
908
  if (e instanceof actions_1.ActionDiagnosticsError)
710
- return reply.code(422).send(e.diagnostics);
909
+ return reply.code(422).send(enrichDiag(e.diagnostics));
711
910
  throw e;
712
911
  }
713
912
  });
@@ -715,31 +914,48 @@ function registerActionRoutes(server, registry) {
715
914
  const s = resolve(req.params.id, reply);
716
915
  if (!s)
717
916
  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)' });
917
+ const { source, source_element_id, source_ref_id, target, target_element_id, target_ref_id, purpose, operator } = req.body;
918
+ const src = resolveTarget({ selector: source, element_id: source_element_id, ref_id: source_ref_id }, reply, s.id);
919
+ if (!src)
920
+ return;
921
+ const tgt = resolveTarget({ selector: target, element_id: target_element_id, ref_id: target_ref_id }, reply, s.id);
922
+ if (!tgt)
923
+ return;
723
924
  try {
724
925
  return await Actions.drag(s.page, src, tgt, getLogger(), s.id, purpose, inferOperator(req, s, operator));
725
926
  }
726
927
  catch (e) {
727
928
  if (e instanceof actions_1.ActionDiagnosticsError)
728
- return reply.code(422).send(e.diagnostics);
929
+ return reply.code(422).send(enrichDiag(e.diagnostics));
729
930
  throw e;
730
931
  }
731
932
  });
933
+ // R08-R05/R08: Ref->Box->Input — mouse_move accepts ref_id + steps for smooth trajectory
732
934
  server.post('/api/v1/sessions/:id/mouse_move', async (req, reply) => {
733
935
  const s = resolve(req.params.id, reply);
734
936
  if (!s)
735
937
  return;
736
- const { x, y, purpose, operator } = req.body;
938
+ const { purpose, operator, steps } = req.body;
939
+ let { x, y } = req.body;
940
+ // Ref->Box->Input: if ref_id/element_id/selector provided, resolve to bbox center coords
941
+ if (req.body.ref_id || req.body.element_id || req.body.selector) {
942
+ const cssSelector = resolveTarget(req.body, reply, s.id);
943
+ if (!cssSelector)
944
+ return;
945
+ const bbox = await s.page.locator(cssSelector).boundingBox();
946
+ if (!bbox)
947
+ return reply.code(404).send({ error: 'Element not found or not visible for mouse_move', selector: cssSelector });
948
+ x = Math.round(bbox.x + bbox.width / 2);
949
+ y = Math.round(bbox.y + bbox.height / 2);
950
+ }
951
+ if (x === undefined || y === undefined)
952
+ return reply.code(400).send({ error: 'Either x+y coordinates or ref_id/element_id/selector is required' });
737
953
  try {
738
- return await Actions.mouseMove(s.page, x, y, getLogger(), s.id, purpose, inferOperator(req, s, operator));
954
+ return await Actions.mouseMove(s.page, x, y, steps ?? 1, getLogger(), s.id, purpose, inferOperator(req, s, operator));
739
955
  }
740
956
  catch (e) {
741
957
  if (e instanceof actions_1.ActionDiagnosticsError)
742
- return reply.code(422).send(e.diagnostics);
958
+ return reply.code(422).send(enrichDiag(e.diagnostics));
743
959
  throw e;
744
960
  }
745
961
  });
@@ -753,7 +969,7 @@ function registerActionRoutes(server, registry) {
753
969
  }
754
970
  catch (e) {
755
971
  if (e instanceof actions_1.ActionDiagnosticsError)
756
- return reply.code(422).send(e.diagnostics);
972
+ return reply.code(422).send(enrichDiag(e.diagnostics));
757
973
  throw e;
758
974
  }
759
975
  });
@@ -767,7 +983,7 @@ function registerActionRoutes(server, registry) {
767
983
  }
768
984
  catch (e) {
769
985
  if (e instanceof actions_1.ActionDiagnosticsError)
770
- return reply.code(422).send(e.diagnostics);
986
+ return reply.code(422).send(enrichDiag(e.diagnostics));
771
987
  throw e;
772
988
  }
773
989
  });
@@ -781,7 +997,7 @@ function registerActionRoutes(server, registry) {
781
997
  }
782
998
  catch (e) {
783
999
  if (e instanceof actions_1.ActionDiagnosticsError)
784
- return reply.code(422).send(e.diagnostics);
1000
+ return reply.code(422).send(enrichDiag(e.diagnostics));
785
1001
  throw e;
786
1002
  }
787
1003
  });
@@ -795,7 +1011,7 @@ function registerActionRoutes(server, registry) {
795
1011
  }
796
1012
  catch (e) {
797
1013
  if (e instanceof actions_1.ActionDiagnosticsError)
798
- return reply.code(422).send(e.diagnostics);
1014
+ return reply.code(422).send(enrichDiag(e.diagnostics));
799
1015
  throw e;
800
1016
  }
801
1017
  });
@@ -812,7 +1028,7 @@ function registerActionRoutes(server, registry) {
812
1028
  }
813
1029
  catch (e) {
814
1030
  if (e instanceof actions_1.ActionDiagnosticsError)
815
- return reply.code(422).send(e.diagnostics);
1031
+ return reply.code(422).send(enrichDiag(e.diagnostics));
816
1032
  throw e;
817
1033
  }
818
1034
  });
@@ -826,7 +1042,7 @@ function registerActionRoutes(server, registry) {
826
1042
  }
827
1043
  catch (e) {
828
1044
  if (e instanceof actions_1.ActionDiagnosticsError)
829
- return reply.code(422).send(e.diagnostics);
1045
+ return reply.code(422).send(enrichDiag(e.diagnostics));
830
1046
  throw e;
831
1047
  }
832
1048
  });
@@ -840,7 +1056,7 @@ function registerActionRoutes(server, registry) {
840
1056
  }
841
1057
  catch (e) {
842
1058
  if (e instanceof actions_1.ActionDiagnosticsError)
843
- return reply.code(422).send(e.diagnostics);
1059
+ return reply.code(422).send(enrichDiag(e.diagnostics));
844
1060
  throw e;
845
1061
  }
846
1062
  });
@@ -857,7 +1073,7 @@ function registerActionRoutes(server, registry) {
857
1073
  }
858
1074
  catch (e) {
859
1075
  if (e instanceof actions_1.ActionDiagnosticsError)
860
- return reply.code(422).send(e.diagnostics);
1076
+ return reply.code(422).send(enrichDiag(e.diagnostics));
861
1077
  throw e;
862
1078
  }
863
1079
  });
@@ -871,7 +1087,7 @@ function registerActionRoutes(server, registry) {
871
1087
  }
872
1088
  catch (e) {
873
1089
  if (e instanceof actions_1.ActionDiagnosticsError)
874
- return reply.code(422).send(e.diagnostics);
1090
+ return reply.code(422).send(enrichDiag(e.diagnostics));
875
1091
  throw e;
876
1092
  }
877
1093
  });
@@ -885,7 +1101,7 @@ function registerActionRoutes(server, registry) {
885
1101
  }
886
1102
  catch (e) {
887
1103
  if (e instanceof actions_1.ActionDiagnosticsError)
888
- return reply.code(422).send(e.diagnostics);
1104
+ return reply.code(422).send(enrichDiag(e.diagnostics));
889
1105
  throw e;
890
1106
  }
891
1107
  });
@@ -899,11 +1115,12 @@ function registerActionRoutes(server, registry) {
899
1115
  const { purpose, operator, ...opts } = req.body ?? {};
900
1116
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
901
1117
  try {
902
- return await Actions.scrollUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1118
+ const result = await Actions.scrollUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1119
+ return { ...result, session_id: s.id };
903
1120
  }
904
1121
  catch (e) {
905
1122
  if (e instanceof actions_1.ActionDiagnosticsError)
906
- return reply.code(422).send(e.diagnostics);
1123
+ return reply.code(422).send(enrichDiag(e.diagnostics));
907
1124
  throw e;
908
1125
  }
909
1126
  });
@@ -913,11 +1130,12 @@ function registerActionRoutes(server, registry) {
913
1130
  return;
914
1131
  const { purpose, operator, ...opts } = req.body;
915
1132
  try {
916
- return await Actions.loadMoreUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1133
+ const result = await Actions.loadMoreUntil(s.page, opts, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1134
+ return { ...result, session_id: s.id };
917
1135
  }
918
1136
  catch (e) {
919
1137
  if (e instanceof actions_1.ActionDiagnosticsError)
920
- return reply.code(422).send(e.diagnostics);
1138
+ return reply.code(422).send(enrichDiag(e.diagnostics));
921
1139
  throw e;
922
1140
  }
923
1141
  });
@@ -925,13 +1143,13 @@ function registerActionRoutes(server, registry) {
925
1143
  // R07-T13: snapshot_map — versioned element scan with page_rev tracking
926
1144
  // ---------------------------------------------------------------------------
927
1145
  server.post('/api/v1/sessions/:id/snapshot_map', async (req, reply) => {
928
- const s = resolve(req.params.id, reply);
1146
+ const s = resolveWithPage(req.params.id, req.body?.page_id, reply);
929
1147
  if (!s)
930
1148
  return;
931
- const { scope, limit = 500, purpose, operator } = req.body ?? {};
1149
+ const { scope, limit = 500, include_unlabeled = false, purpose, operator } = req.body ?? {};
932
1150
  const bm = server.browserManager;
933
1151
  try {
934
- const elemResult = await Actions.elementMap(s.page, { scope, limit }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1152
+ const elemResult = await Actions.elementMap(s.page, { scope, limit, include_unlabeled }, getLogger(), s.id, purpose, inferOperator(req, s, operator));
935
1153
  const snapshotId = 'snap_' + crypto_1.default.randomBytes(4).toString('hex');
936
1154
  const pageRev = bm?.getPageRev(s.id) ?? 0;
937
1155
  const elements = elemResult.elements.map((el) => ({ ...el, ref_id: `${snapshotId}:${el.element_id}` }));
@@ -940,9 +1158,229 @@ function registerActionRoutes(server, registry) {
940
1158
  }
941
1159
  catch (e) {
942
1160
  if (e instanceof actions_1.ActionDiagnosticsError)
943
- return reply.code(422).send(e.diagnostics);
1161
+ return reply.code(422).send(enrichDiag(e.diagnostics));
1162
+ throw e;
1163
+ }
1164
+ });
1165
+ // ---------------------------------------------------------------------------
1166
+ // R08-R10: Semantic element locator — find by role/text/label/placeholder/alt_text
1167
+ // ---------------------------------------------------------------------------
1168
+ server.post('/api/v1/sessions/:id/find', async (req, reply) => {
1169
+ const s = resolve(req.params.id, reply);
1170
+ if (!s)
1171
+ return;
1172
+ const { query_type, query, name, exact = false, nth = 0 } = req.body;
1173
+ try {
1174
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1175
+ let locator;
1176
+ if (query_type === 'role')
1177
+ locator = s.page.getByRole(query, { name, exact });
1178
+ else if (query_type === 'text')
1179
+ locator = s.page.getByText(query, { exact });
1180
+ else if (query_type === 'label')
1181
+ locator = s.page.getByLabel(query, { exact });
1182
+ else if (query_type === 'placeholder')
1183
+ locator = s.page.getByPlaceholder(query, { exact });
1184
+ else if (query_type === 'alt_text')
1185
+ locator = s.page.getByAltText(query, { exact });
1186
+ else
1187
+ return reply.code(400).send({ error: `Unknown query_type: ${query_type}`, valid: ['role', 'text', 'label', 'placeholder', 'alt_text'] });
1188
+ const count = await locator.count();
1189
+ if (count === 0)
1190
+ return { status: 'ok', found: false, count: 0, nth: 0, query_type, query };
1191
+ const target = locator.nth(nth);
1192
+ const bbox = await target.boundingBox().catch(() => null);
1193
+ const text = await target.textContent().catch(() => null);
1194
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1195
+ const tag = await target.evaluate((el) => el.tagName.toLowerCase()).catch(() => null);
1196
+ return { status: 'ok', found: true, count, nth, query_type, query, tag, text: text?.trim() || null, bbox };
1197
+ }
1198
+ catch (e) {
1199
+ if (e instanceof actions_1.ActionDiagnosticsError)
1200
+ return reply.code(422).send(enrichDiag(e.diagnostics));
1201
+ throw e;
1202
+ }
1203
+ });
1204
+ // ---------------------------------------------------------------------------
1205
+ // R08-R16: Upload URL — fetch remote URL and upload as file input
1206
+ // ---------------------------------------------------------------------------
1207
+ server.post('/api/v1/sessions/:id/upload_url', async (req, reply) => {
1208
+ const s = resolve(req.params.id, reply);
1209
+ if (!s)
1210
+ return;
1211
+ const { url, filename: filenameHint, mime_type, purpose, operator } = req.body;
1212
+ if (!url)
1213
+ return reply.code(400).send({ error: 'url is required' });
1214
+ const cssSelector = resolveTarget(req.body, reply, s.id);
1215
+ if (!cssSelector)
1216
+ return;
1217
+ let resp;
1218
+ try {
1219
+ resp = await fetch(url);
1220
+ }
1221
+ catch (e) {
1222
+ return reply.code(422).send({ error: 'fetch_failed', message: e.message, url });
1223
+ }
1224
+ if (!resp.ok)
1225
+ return reply.code(422).send({ error: 'fetch_failed', http_status: resp.status, url });
1226
+ const buf = Buffer.from(await resp.arrayBuffer());
1227
+ const filename = filenameHint ?? url.split('/').pop()?.split('?')[0] ?? 'file';
1228
+ const ext = path_1.default.extname(filename) || '.bin';
1229
+ const tmpPath = path_1.default.join(os_1.default.tmpdir(), `agentmb_${crypto_1.default.randomBytes(4).toString('hex')}${ext}`);
1230
+ try {
1231
+ await fs_1.default.promises.writeFile(tmpPath, buf);
1232
+ const b64 = buf.toString('base64');
1233
+ const result = await Actions.uploadFile(s.page, cssSelector, b64, filename, mime_type, getLogger(), s.id, purpose, inferOperator(req, s, operator));
1234
+ return { ...result, url, fetched_bytes: buf.length };
1235
+ }
1236
+ catch (e) {
1237
+ if (e instanceof actions_1.ActionDiagnosticsError)
1238
+ return reply.code(422).send(enrichDiag(e.diagnostics));
944
1239
  throw e;
945
1240
  }
1241
+ finally {
1242
+ fs_1.default.promises.unlink(tmpPath).catch(() => { });
1243
+ }
1244
+ });
1245
+ // ---------------------------------------------------------------------------
1246
+ // R08-R18: run_steps — batch action dispatcher
1247
+ // Supported actions: navigate, click, fill, type, press, hover, scroll,
1248
+ // wait_for_selector, wait_text, screenshot, eval
1249
+ // r08-c07: resolveRefIdForStep — throws (instead of reply) for use inside step loops
1250
+ // ---------------------------------------------------------------------------
1251
+ function resolveRefIdForStep(params, sessionId) {
1252
+ if (params.ref_id) {
1253
+ const bm = server.browserManager;
1254
+ if (!bm)
1255
+ throw new Error('ref_id resolution requires BrowserManager');
1256
+ const colonIdx = params.ref_id.lastIndexOf(':');
1257
+ if (colonIdx === -1)
1258
+ throw new Error(`Invalid ref_id format: "${params.ref_id}"; expected "snap_XXXXXX:eN"`);
1259
+ const snapshotId = params.ref_id.slice(0, colonIdx);
1260
+ const eid = params.ref_id.slice(colonIdx + 1);
1261
+ const snapshot = bm.getSnapshot(sessionId, snapshotId);
1262
+ if (!snapshot)
1263
+ throw new Error(`stale_ref: snapshot "${snapshotId}" not found or expired; call snapshot_map again`);
1264
+ const currentRev = bm.getPageRev(sessionId);
1265
+ if (snapshot.page_rev !== currentRev)
1266
+ throw new Error(`stale_ref: page changed (snapshot_rev=${snapshot.page_rev}, current=${currentRev}); call snapshot_map again`);
1267
+ return `[data-agentmb-eid="${eid}"]`;
1268
+ }
1269
+ if (params.element_id)
1270
+ return `[data-agentmb-eid="${params.element_id}"]`;
1271
+ return params.selector;
1272
+ }
1273
+ server.post('/api/v1/sessions/:id/run_steps', async (req, reply) => {
1274
+ const s = resolve(req.params.id, reply);
1275
+ if (!s)
1276
+ return;
1277
+ const { steps, stop_on_error = true, purpose, operator } = req.body;
1278
+ if (!Array.isArray(steps) || steps.length === 0)
1279
+ return reply.code(400).send({ error: 'steps must be a non-empty array' });
1280
+ if (steps.length > 100)
1281
+ return reply.code(400).send({ error: 'steps must not exceed 100' });
1282
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1283
+ const results = [];
1284
+ const op = inferOperator(req, s, operator);
1285
+ for (let i = 0; i < steps.length; i++) {
1286
+ const step = steps[i];
1287
+ const { action, params = {} } = step;
1288
+ const stepPurpose = params.purpose ?? purpose;
1289
+ try {
1290
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1291
+ let result;
1292
+ switch (action) {
1293
+ case 'navigate':
1294
+ result = await Actions.navigate(s.page, params.url, params.wait_until ?? 'load', getLogger(), s.id, stepPurpose, op);
1295
+ break;
1296
+ case 'click': {
1297
+ if (!params.selector && !params.element_id && !params.ref_id)
1298
+ throw new Error('click requires selector, element_id, or ref_id');
1299
+ const sel = resolveRefIdForStep(params, s.id);
1300
+ result = await Actions.click(s.page, sel, params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
1301
+ break;
1302
+ }
1303
+ case 'fill': {
1304
+ if (!params.selector && !params.element_id && !params.ref_id)
1305
+ throw new Error('fill requires selector, element_id, or ref_id');
1306
+ const sel = resolveRefIdForStep(params, s.id);
1307
+ result = await Actions.fill(s.page, sel, params.value ?? '', getLogger(), s.id, stepPurpose, op);
1308
+ break;
1309
+ }
1310
+ case 'type': {
1311
+ if (!params.selector && !params.element_id && !params.ref_id)
1312
+ throw new Error('type requires selector, element_id, or ref_id');
1313
+ const sel = resolveRefIdForStep(params, s.id);
1314
+ result = await Actions.typeText(s.page, sel, params.text ?? '', params.delay_ms ?? 0, getLogger(), s.id, stepPurpose, op);
1315
+ break;
1316
+ }
1317
+ case 'press': {
1318
+ if (!params.selector && !params.element_id && !params.ref_id)
1319
+ throw new Error('press requires selector, element_id, or ref_id');
1320
+ const sel = resolveRefIdForStep(params, s.id);
1321
+ result = await Actions.press(s.page, sel, params.key ?? '', getLogger(), s.id, stepPurpose, op);
1322
+ break;
1323
+ }
1324
+ case 'hover': {
1325
+ if (!params.selector && !params.element_id && !params.ref_id)
1326
+ throw new Error('hover requires selector, element_id, or ref_id');
1327
+ const sel = resolveRefIdForStep(params, s.id);
1328
+ result = await Actions.hover(s.page, sel, getLogger(), s.id, stepPurpose, op);
1329
+ break;
1330
+ }
1331
+ case 'scroll': {
1332
+ if (!params.selector && !params.element_id && !params.ref_id)
1333
+ throw new Error('scroll requires selector, element_id, or ref_id');
1334
+ const sel = resolveRefIdForStep(params, s.id);
1335
+ result = await Actions.scroll(s.page, sel, { delta_x: params.delta_x ?? 0, delta_y: params.delta_y ?? 300 }, getLogger(), s.id, stepPurpose, op);
1336
+ break;
1337
+ }
1338
+ case 'wait_for_selector':
1339
+ result = await Actions.waitForSelector(s.page, params.selector, params.state ?? 'visible', params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
1340
+ break;
1341
+ case 'wait_text':
1342
+ result = await Actions.waitForText(s.page, params.text, params.timeout_ms ?? 5000, getLogger(), s.id, stepPurpose, op);
1343
+ break;
1344
+ case 'screenshot':
1345
+ result = await Actions.screenshot(s.page, params.format ?? 'png', params.full_page ?? false, getLogger(), s.id, stepPurpose, op);
1346
+ break;
1347
+ case 'eval':
1348
+ result = await Actions.evaluate(s.page, params.expression, getLogger(), s.id, stepPurpose, op);
1349
+ break;
1350
+ default:
1351
+ throw new Error(`unsupported action: ${action}`);
1352
+ }
1353
+ results.push({ step: i + 1, action, result });
1354
+ }
1355
+ catch (e) {
1356
+ const errPayload = e instanceof actions_1.ActionDiagnosticsError
1357
+ ? enrichDiag(e.diagnostics)
1358
+ : { error: e.message ?? String(e) };
1359
+ results.push({ step: i + 1, action, error: errPayload });
1360
+ if (stop_on_error)
1361
+ break;
1362
+ }
1363
+ }
1364
+ const failed = results.filter(r => r.error);
1365
+ return {
1366
+ status: failed.length === 0 ? 'ok' : (results.filter(r => r.result).length > 0 ? 'partial' : 'failed'),
1367
+ total_steps: steps.length,
1368
+ completed_steps: results.filter(r => r.result).length,
1369
+ failed_steps: failed.length,
1370
+ results,
1371
+ };
1372
+ });
1373
+ // ---------------------------------------------------------------------------
1374
+ // R08-R12: Snapshot Ref 强化 — GET page_rev endpoint
1375
+ // Lets clients cheaply check if the page has changed since last snapshot.
1376
+ // ---------------------------------------------------------------------------
1377
+ server.get('/api/v1/sessions/:id/page_rev', async (req, reply) => {
1378
+ const s = resolve(req.params.id, reply);
1379
+ if (!s)
1380
+ return;
1381
+ const bm = server.browserManager;
1382
+ const page_rev = bm?.getPageRev(s.id) ?? 0;
1383
+ return { status: 'ok', session_id: req.params.id, page_rev, url: s.page.url() };
946
1384
  });
947
1385
  }
948
1386
  //# sourceMappingURL=actions.js.map