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
@@ -11,6 +11,27 @@ const client_1 = require("../client");
11
11
  function collectValues(val, prev) {
12
12
  return prev.concat([val]);
13
13
  }
14
+ // T09: extension → MIME mapping for upload auto-inference
15
+ const EXT_TO_MIME = {
16
+ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
17
+ webp: 'image/webp', avif: 'image/avif', svg: 'image/svg+xml',
18
+ pdf: 'application/pdf',
19
+ mp4: 'video/mp4', mov: 'video/quicktime', avi: 'video/x-msvideo', webm: 'video/webm',
20
+ mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4',
21
+ txt: 'text/plain', csv: 'text/csv', html: 'text/html', css: 'text/css', js: 'text/javascript',
22
+ json: 'application/json', xml: 'application/xml',
23
+ zip: 'application/zip', gz: 'application/gzip', tar: 'application/x-tar',
24
+ doc: 'application/msword',
25
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
26
+ xls: 'application/vnd.ms-excel',
27
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
28
+ ppt: 'application/vnd.ms-powerpoint',
29
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
30
+ };
31
+ function inferMime(filePath) {
32
+ const ext = path_1.default.extname(filePath).slice(1).toLowerCase();
33
+ return EXT_TO_MIME[ext] ?? 'application/octet-stream';
34
+ }
14
35
  function printDiagnostics(res) {
15
36
  console.error('Error:', res.error);
16
37
  if (res.url)
@@ -29,11 +50,12 @@ function actionCommands(program) {
29
50
  .command('navigate <session-id> <url>')
30
51
  .description('Navigate to URL')
31
52
  .option('--wait-until <event>', 'Wait until event (load|networkidle|commit)', 'load')
53
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
32
54
  .action(async (sessionId, url, opts) => {
33
- const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/navigate`, {
34
- url,
35
- wait_until: opts.waitUntil,
36
- });
55
+ const body = { url, wait_until: opts.waitUntil };
56
+ if (opts.pageId)
57
+ body.page_id = opts.pageId;
58
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/navigate`, body);
37
59
  if (res.error) {
38
60
  console.error('Error:', res.error);
39
61
  process.exit(1);
@@ -47,11 +69,12 @@ function actionCommands(program) {
47
69
  .option('-o, --out <file>', 'Output file path', './screenshot.png')
48
70
  .option('--full-page', 'Capture full page')
49
71
  .option('--format <fmt>', 'Format: png|jpeg', 'png')
72
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
50
73
  .action(async (sessionId, opts) => {
51
- const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/screenshot`, {
52
- format: opts.format,
53
- full_page: opts.fullPage,
54
- });
74
+ const body = { format: opts.format, full_page: opts.fullPage };
75
+ if (opts.pageId)
76
+ body.page_id = opts.pageId;
77
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/screenshot`, body);
55
78
  if (res.error) {
56
79
  printDiagnostics(res);
57
80
  process.exit(1);
@@ -63,8 +86,12 @@ function actionCommands(program) {
63
86
  program
64
87
  .command('eval <session-id> <expression>')
65
88
  .description('Evaluate JavaScript expression in browser')
66
- .action(async (sessionId, expression) => {
67
- const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/eval`, { expression });
89
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
90
+ .action(async (sessionId, expression, opts) => {
91
+ const body = { expression };
92
+ if (opts.pageId)
93
+ body.page_id = opts.pageId;
94
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/eval`, body);
68
95
  if (res.error) {
69
96
  printDiagnostics(res);
70
97
  process.exit(1);
@@ -91,11 +118,16 @@ function actionCommands(program) {
91
118
  });
92
119
  program
93
120
  .command('click <session-id> <selector-or-eid>')
94
- .description('Click an element (use --element-id to treat arg as element_id from element-map)')
121
+ .description('Click an element (use --element-id or --ref-id to identify element)')
95
122
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
123
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
124
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
96
125
  .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 };
126
+ const body = opts.refId
127
+ ? { ref_id: selectorOrEid }
128
+ : opts.elementId ? { element_id: selectorOrEid } : { selector: selectorOrEid };
129
+ if (opts.pageId)
130
+ body.page_id = opts.pageId;
99
131
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/click`, body);
100
132
  if (res.error) {
101
133
  console.error('Error:', res.error);
@@ -105,13 +137,25 @@ function actionCommands(program) {
105
137
  });
106
138
  program
107
139
  .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)')
140
+ .description('Fill a form field (use --element-id or --ref-id to identify element)')
109
141
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
142
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
143
+ .option('--fill-strategy <type>', 'Fill strategy: normal (default) | type (simulate keystrokes)', 'normal')
144
+ .option('--char-delay-ms <ms>', 'Delay between characters when --fill-strategy=type', '0')
145
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
110
146
  .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 };
147
+ const body = opts.refId
148
+ ? { ref_id: selectorOrEid, value }
149
+ : opts.elementId
150
+ ? { element_id: selectorOrEid, value }
151
+ : { selector: selectorOrEid, value };
152
+ if (opts.fillStrategy && opts.fillStrategy !== 'normal')
153
+ body.fill_strategy = opts.fillStrategy;
154
+ const charDelay = parseInt(opts.charDelayMs ?? '0');
155
+ if (charDelay > 0)
156
+ body.char_delay_ms = charDelay;
157
+ if (opts.pageId)
158
+ body.page_id = opts.pageId;
115
159
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/fill`, body);
116
160
  if (res.error) {
117
161
  console.error('Error:', res.error);
@@ -155,27 +199,48 @@ function actionCommands(program) {
155
199
  console.log(`✓ Session ${sessionId} returned to headless mode. Automation can resume.`);
156
200
  });
157
201
  program
158
- .command('type <session-id> <selector> <text>')
159
- .description('Type text into an element character by character')
202
+ .command('type <session-id> <selector-or-eid> <text>')
203
+ .description('Type text character by character (use --element-id or --ref-id to identify element)')
204
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
205
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
160
206
  .option('--delay-ms <ms>', 'Delay between keystrokes (ms)', '0')
161
- .action(async (sessionId, selector, text, opts) => {
162
- const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/type`, { selector, text, delay_ms: parseInt(opts.delayMs) });
207
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
208
+ .action(async (sessionId, selectorOrEid, text, opts) => {
209
+ const delay_ms = parseInt(opts.delayMs);
210
+ const body = opts.refId
211
+ ? { ref_id: selectorOrEid, text, delay_ms }
212
+ : opts.elementId
213
+ ? { element_id: selectorOrEid, text, delay_ms }
214
+ : { selector: selectorOrEid, text, delay_ms };
215
+ if (opts.pageId)
216
+ body.page_id = opts.pageId;
217
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/type`, body);
163
218
  if (res.error) {
164
219
  printDiagnostics(res);
165
220
  process.exit(1);
166
221
  }
167
- console.log(`✓ Typed into "${selector}" (${res.duration_ms}ms)`);
222
+ console.log(`✓ Typed into "${selectorOrEid}" (${res.duration_ms}ms)`);
168
223
  });
169
224
  program
170
- .command('press <session-id> <selector> <key>')
171
- .description('Press a key or combo (e.g. Enter, Tab, Control+a)')
172
- .action(async (sessionId, selector, key) => {
173
- const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/press`, { selector, key });
225
+ .command('press <session-id> <selector-or-eid> <key>')
226
+ .description('Press a key or combo (e.g. Enter, Tab, Control+a) (use --element-id or --ref-id to identify element)')
227
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
228
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
229
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
230
+ .action(async (sessionId, selectorOrEid, key, opts) => {
231
+ const body = opts.refId
232
+ ? { ref_id: selectorOrEid, key }
233
+ : opts.elementId
234
+ ? { element_id: selectorOrEid, key }
235
+ : { selector: selectorOrEid, key };
236
+ if (opts.pageId)
237
+ body.page_id = opts.pageId;
238
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/press`, body);
174
239
  if (res.error) {
175
240
  printDiagnostics(res);
176
241
  process.exit(1);
177
242
  }
178
- console.log(`✓ Pressed "${key}" on "${selector}" (${res.duration_ms}ms)`);
243
+ console.log(`✓ Pressed "${key}" on "${selectorOrEid}" (${res.duration_ms}ms)`);
179
244
  });
180
245
  program
181
246
  .command('select <session-id> <selector> <value...>')
@@ -189,15 +254,22 @@ function actionCommands(program) {
189
254
  console.log(`✓ Selected [${res.selected.join(', ')}] in "${selector}" (${res.duration_ms}ms)`);
190
255
  });
191
256
  program
192
- .command('hover <session-id> <selector>')
193
- .description('Hover over an element')
194
- .action(async (sessionId, selector) => {
195
- const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/hover`, { selector });
257
+ .command('hover <session-id> <selector-or-eid>')
258
+ .description('Hover over an element (use --element-id or --ref-id to identify element)')
259
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
260
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
261
+ .action(async (sessionId, selectorOrEid, opts) => {
262
+ const body = opts.refId
263
+ ? { ref_id: selectorOrEid }
264
+ : opts.elementId
265
+ ? { element_id: selectorOrEid }
266
+ : { selector: selectorOrEid };
267
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/hover`, body);
196
268
  if (res.error) {
197
269
  printDiagnostics(res);
198
270
  process.exit(1);
199
271
  }
200
- console.log(`✓ Hovered over "${selector}" (${res.duration_ms}ms)`);
272
+ console.log(`✓ Hovered over "${selectorOrEid}" (${res.duration_ms}ms)`);
201
273
  });
202
274
  program
203
275
  .command('wait-selector <session-id> <selector>')
@@ -246,33 +318,45 @@ function actionCommands(program) {
246
318
  });
247
319
  program
248
320
  .command('upload <session-id> <selector> <file>')
249
- .description('Upload a local file to a file input element')
250
- .option('--mime-type <type>', 'MIME type', 'application/octet-stream')
321
+ .description('Upload a local file to a file input element (MIME auto-inferred from extension)')
322
+ .option('--mime-type <type>', 'Override MIME type (default: inferred from file extension)')
251
323
  .action(async (sessionId, selector, file, opts) => {
252
324
  const buf = fs_1.default.readFileSync(file);
325
+ const mime_type = opts.mimeType ?? inferMime(file);
253
326
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/upload`, {
254
327
  selector,
255
328
  content: buf.toString('base64'),
256
329
  filename: path_1.default.basename(file),
257
- mime_type: opts.mimeType,
330
+ mime_type,
258
331
  });
259
332
  if (res.error) {
260
333
  printDiagnostics(res);
261
334
  process.exit(1);
262
335
  }
263
- console.log(`✓ Uploaded "${res.filename}" (${res.size_bytes} bytes, ${res.duration_ms}ms)`);
336
+ console.log(`✓ Uploaded "${res.filename}" (${res.size_bytes} bytes, mime=${mime_type}, ${res.duration_ms}ms)`);
264
337
  });
265
338
  program
266
- .command('download <session-id> <selector>')
267
- .description('Click a download link and save the file')
339
+ .command('download <session-id> <selector-or-eid>')
340
+ .description('Click a download link and save the file (requires session --accept-downloads; use --element-id or --ref-id to identify element)')
268
341
  .option('-o, --out <file>', 'Output file path (default: suggested filename)')
269
342
  .option('--timeout-ms <ms>', 'Timeout in ms', '30000')
270
- .action(async (sessionId, selector, opts) => {
271
- const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/download`, {
272
- selector, timeout_ms: parseInt(opts.timeoutMs),
273
- });
343
+ .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
344
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
345
+ .action(async (sessionId, selectorOrEid, opts) => {
346
+ const body = opts.refId
347
+ ? { ref_id: selectorOrEid }
348
+ : opts.elementId ? { element_id: selectorOrEid } : { selector: selectorOrEid };
349
+ body.timeout_ms = parseInt(opts.timeoutMs);
350
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/download`, body);
274
351
  if (res.error) {
275
- printDiagnostics(res);
352
+ if (res.error === 'download_not_enabled') {
353
+ console.error('Error: Downloads are disabled for this session.');
354
+ console.error(' Create the session with --accept-downloads flag:');
355
+ console.error(' agentmb session new --accept-downloads');
356
+ }
357
+ else {
358
+ printDiagnostics(res);
359
+ }
276
360
  process.exit(1);
277
361
  }
278
362
  const outPath = opts.out ?? res.filename;
@@ -361,14 +445,20 @@ function actionCommands(program) {
361
445
  // ---------------------------------------------------------------------------
362
446
  program
363
447
  .command('element-map <session-id>')
364
- .description('Scan the page and return a numbered element map (assigns stable element IDs)')
448
+ .description('Scan the page and return a numbered element map (assigns stable element IDs). Note: icon-only elements without aria-label/title/text will have an empty label unless --include-unlabeled is set.')
365
449
  .option('--scope <selector>', 'Limit scan to elements inside this CSS selector')
366
450
  .option('--limit <n>', 'Max elements to return', '500')
451
+ .option('--include-unlabeled', 'Include icon-only elements with no accessible text; synthesizes [tag @ x,y] label as fallback')
367
452
  .option('--json', 'Output raw JSON instead of a table')
453
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
368
454
  .action(async (sessionId, opts) => {
369
455
  const body = { limit: parseInt(opts.limit) };
370
456
  if (opts.scope)
371
457
  body.scope = opts.scope;
458
+ if (opts.includeUnlabeled)
459
+ body.include_unlabeled = true;
460
+ if (opts.pageId)
461
+ body.page_id = opts.pageId;
372
462
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/element_map`, body);
373
463
  if (res.error) {
374
464
  console.error('Error:', res.error);
@@ -386,8 +476,9 @@ function actionCommands(program) {
386
476
  console.log(`Found ${elements.length} element(s) on ${res.url}:`);
387
477
  for (const el of elements) {
388
478
  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}`);
479
+ const label = String(el.label ?? el.text ?? '').slice(0, 60).replace(/\n/g, ' ');
480
+ const src = el.label_source && el.label_source !== 'none' ? ` [${el.label_source}]` : '';
481
+ console.log(` ${el.element_id} <${el.tag}> role=${el.role}${blocked}${src} ${label}`);
391
482
  }
392
483
  });
393
484
  program
@@ -395,9 +486,13 @@ function actionCommands(program) {
395
486
  .description('Read a property from an element (text|html|value|attr|count|box)')
396
487
  .option('--attr-name <name>', 'Attribute name (required when property=attr)')
397
488
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
489
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
398
490
  .action(async (sessionId, property, target, opts) => {
399
491
  const body = { property };
400
- if (opts.elementId) {
492
+ if (opts.refId) {
493
+ body.ref_id = target;
494
+ }
495
+ else if (opts.elementId) {
401
496
  body.element_id = target;
402
497
  }
403
498
  else {
@@ -416,10 +511,14 @@ function actionCommands(program) {
416
511
  .command('assert <session-id> <property> <selector-or-eid>')
417
512
  .description('Assert element state: visible|enabled|checked')
418
513
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
514
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
419
515
  .option('--expected <bool>', 'Expected value (true|false)', 'true')
420
516
  .action(async (sessionId, property, target, opts) => {
421
517
  const body = { property, expected: opts.expected !== 'false' };
422
- if (opts.elementId) {
518
+ if (opts.refId) {
519
+ body.ref_id = target;
520
+ }
521
+ else if (opts.elementId) {
423
522
  body.element_id = target;
424
523
  }
425
524
  else {
@@ -460,14 +559,22 @@ function actionCommands(program) {
460
559
  // ---------------------------------------------------------------------------
461
560
  program
462
561
  .command('snapshot-map <session-id>')
463
- .description('Snapshot the page element map with page_rev tracking (returns ref_ids for stable targeting)')
562
+ .description('Snapshot the page element map with page_rev tracking (returns ref_ids for stable targeting).\n' +
563
+ 'Limitation: elements with no accessible text (aria-label/title/placeholder/innerText) will have\n' +
564
+ 'an empty label. Use --include-unlabeled to synthesize a [tag @ x,y] fallback for icon-only elements.')
464
565
  .option('--scope <selector>', 'Limit scan to elements inside this CSS selector')
465
566
  .option('--limit <n>', 'Max elements to return', '500')
567
+ .option('--include-unlabeled', 'Include icon-only elements with no accessible text; synthesizes [tag @ x,y] fallback label')
466
568
  .option('--json', 'Output raw JSON instead of a table')
569
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
467
570
  .action(async (sessionId, opts) => {
468
571
  const body = { limit: parseInt(opts.limit) };
469
572
  if (opts.scope)
470
573
  body.scope = opts.scope;
574
+ if (opts.includeUnlabeled)
575
+ body.include_unlabeled = true;
576
+ if (opts.pageId)
577
+ body.page_id = opts.pageId;
471
578
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/snapshot_map`, body);
472
579
  if (res.error) {
473
580
  console.error('Error:', res.error);
@@ -481,8 +588,9 @@ function actionCommands(program) {
481
588
  console.log(`Snapshot ${res.snapshot_id} (page_rev=${res.page_rev}) — ${elements.length} element(s) on ${res.url}:`);
482
589
  for (const el of elements) {
483
590
  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}`);
591
+ const label = String(el.label ?? el.text ?? '').slice(0, 60).replace(/\n/g, ' ');
592
+ const src = el.label_source && el.label_source !== 'none' ? ` [${el.label_source}]` : '';
593
+ console.log(` ${el.ref_id} <${el.tag}> role=${el.role}${blocked}${src} ${label}`);
486
594
  }
487
595
  });
488
596
  // ---------------------------------------------------------------------------
@@ -492,11 +600,14 @@ function actionCommands(program) {
492
600
  .command('dblclick <session-id> <selector-or-eid>')
493
601
  .description('Double-click an element')
494
602
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
603
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
495
604
  .option('--timeout-ms <ms>', 'Timeout in ms', '5000')
496
605
  .action(async (sessionId, selectorOrEid, opts) => {
497
- const body = opts.elementId
498
- ? { element_id: selectorOrEid }
499
- : { selector: selectorOrEid };
606
+ const body = opts.refId
607
+ ? { ref_id: selectorOrEid }
608
+ : opts.elementId
609
+ ? { element_id: selectorOrEid }
610
+ : { selector: selectorOrEid };
500
611
  body.timeout_ms = parseInt(opts.timeoutMs);
501
612
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/dblclick`, body);
502
613
  if (res.error) {
@@ -509,10 +620,13 @@ function actionCommands(program) {
509
620
  .command('focus <session-id> <selector-or-eid>')
510
621
  .description('Focus an element')
511
622
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
623
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
512
624
  .action(async (sessionId, selectorOrEid, opts) => {
513
- const body = opts.elementId
514
- ? { element_id: selectorOrEid }
515
- : { selector: selectorOrEid };
625
+ const body = opts.refId
626
+ ? { ref_id: selectorOrEid }
627
+ : opts.elementId
628
+ ? { element_id: selectorOrEid }
629
+ : { selector: selectorOrEid };
516
630
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/focus`, body);
517
631
  if (res.error) {
518
632
  console.error('Error:', res.error);
@@ -524,10 +638,13 @@ function actionCommands(program) {
524
638
  .command('check <session-id> <selector-or-eid>')
525
639
  .description('Check a checkbox or radio button')
526
640
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
641
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
527
642
  .action(async (sessionId, selectorOrEid, opts) => {
528
- const body = opts.elementId
529
- ? { element_id: selectorOrEid }
530
- : { selector: selectorOrEid };
643
+ const body = opts.refId
644
+ ? { ref_id: selectorOrEid }
645
+ : opts.elementId
646
+ ? { element_id: selectorOrEid }
647
+ : { selector: selectorOrEid };
531
648
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/check`, body);
532
649
  if (res.error) {
533
650
  console.error('Error:', res.error);
@@ -539,10 +656,13 @@ function actionCommands(program) {
539
656
  .command('uncheck <session-id> <selector-or-eid>')
540
657
  .description('Uncheck a checkbox')
541
658
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
659
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
542
660
  .action(async (sessionId, selectorOrEid, opts) => {
543
- const body = opts.elementId
544
- ? { element_id: selectorOrEid }
545
- : { selector: selectorOrEid };
661
+ const body = opts.refId
662
+ ? { ref_id: selectorOrEid }
663
+ : opts.elementId
664
+ ? { element_id: selectorOrEid }
665
+ : { selector: selectorOrEid };
546
666
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/uncheck`, body);
547
667
  if (res.error) {
548
668
  console.error('Error:', res.error);
@@ -554,14 +674,20 @@ function actionCommands(program) {
554
674
  .command('scroll <session-id> <selector-or-eid>')
555
675
  .description('Scroll an element by delta pixels')
556
676
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
677
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
557
678
  .option('--dx <px>', 'Horizontal scroll delta', '0')
558
679
  .option('--dy <px>', 'Vertical scroll delta', '300')
680
+ .option('--page-id <id>', 'Target a specific page/tab by page_id (default: active tab)')
559
681
  .action(async (sessionId, selectorOrEid, opts) => {
560
- const body = opts.elementId
561
- ? { element_id: selectorOrEid }
562
- : { selector: selectorOrEid };
682
+ const body = opts.refId
683
+ ? { ref_id: selectorOrEid }
684
+ : opts.elementId
685
+ ? { element_id: selectorOrEid }
686
+ : { selector: selectorOrEid };
563
687
  body.delta_x = parseInt(opts.dx);
564
688
  body.delta_y = parseInt(opts.dy);
689
+ if (opts.pageId)
690
+ body.page_id = opts.pageId;
565
691
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/scroll`, body);
566
692
  if (res.error) {
567
693
  console.error('Error:', res.error);
@@ -573,10 +699,13 @@ function actionCommands(program) {
573
699
  .command('scroll-into-view <session-id> <selector-or-eid>')
574
700
  .description('Scroll element into view')
575
701
  .option('--element-id', 'Treat selector-or-eid as an element_id from element-map')
702
+ .option('--ref-id', 'Treat selector-or-eid as a snapshot ref_id (snap_XXXXXX:eN)')
576
703
  .action(async (sessionId, selectorOrEid, opts) => {
577
- const body = opts.elementId
578
- ? { element_id: selectorOrEid }
579
- : { selector: selectorOrEid };
704
+ const body = opts.refId
705
+ ? { ref_id: selectorOrEid }
706
+ : opts.elementId
707
+ ? { element_id: selectorOrEid }
708
+ : { selector: selectorOrEid };
580
709
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/scroll_into_view`, body);
581
710
  if (res.error) {
582
711
  console.error('Error:', res.error);
@@ -586,26 +715,68 @@ function actionCommands(program) {
586
715
  });
587
716
  program
588
717
  .command('drag <session-id> <source> <target>')
589
- .description('Drag an element from source to target (CSS selectors)')
718
+ .description('Drag from source to target. <source>/<target> are CSS selectors unless --source-ref-id/--target-ref-id used.')
719
+ .option('--source-ref-id <ref_id>', 'Use snapshot ref_id as drag source instead of CSS selector')
720
+ .option('--target-ref-id <ref_id>', 'Use snapshot ref_id as drag target instead of CSS selector')
590
721
  .option('--timeout-ms <ms>', 'Timeout in ms', '5000')
591
722
  .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) });
723
+ const body = { timeout_ms: parseInt(opts.timeoutMs) };
724
+ if (opts.sourceRefId) {
725
+ body.source_ref_id = opts.sourceRefId;
726
+ }
727
+ else {
728
+ body.source = source;
729
+ }
730
+ if (opts.targetRefId) {
731
+ body.target_ref_id = opts.targetRefId;
732
+ }
733
+ else {
734
+ body.target = target;
735
+ }
736
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/drag`, body);
593
737
  if (res.error) {
594
738
  console.error('Error:', res.error);
595
739
  process.exit(1);
596
740
  }
597
- console.log(`✓ Dragged "${source}" "${target}" (${res.duration_ms}ms)`);
741
+ const srcLabel = opts.sourceRefId ?? source;
742
+ const tgtLabel = opts.targetRefId ?? target;
743
+ console.log(`✓ Dragged "${srcLabel}" → "${tgtLabel}" (${res.duration_ms}ms)`);
598
744
  });
599
745
  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) });
746
+ .command('mouse-move <session-id> [x] [y]')
747
+ .description('Move mouse to coordinates or element center (use --selector/--element-id/--ref-id to resolve from element)')
748
+ .option('--selector <sel>', 'Move to center of element matching CSS selector')
749
+ .option('--element-id <id>', 'Move to center of element by element_id from element-map')
750
+ .option('--ref-id <id>', 'Move to center of element by snapshot ref_id (snap_XXXXXX:eN)')
751
+ .option('--steps <n>', 'Number of intermediate mouse steps for smooth movement', '1')
752
+ .action(async (sessionId, x, y, opts) => {
753
+ const body = {};
754
+ if (opts.refId) {
755
+ body.ref_id = opts.refId;
756
+ }
757
+ else if (opts.elementId) {
758
+ body.element_id = opts.elementId;
759
+ }
760
+ else if (opts.selector) {
761
+ body.selector = opts.selector;
762
+ }
763
+ else if (x !== undefined && y !== undefined) {
764
+ body.x = parseFloat(x);
765
+ body.y = parseFloat(y);
766
+ }
767
+ else {
768
+ console.error('Error: provide x/y or --selector/--element-id/--ref-id');
769
+ process.exit(1);
770
+ }
771
+ const steps = parseInt(opts.steps ?? '1');
772
+ if (steps > 1)
773
+ body.steps = steps;
774
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/mouse_move`, body);
604
775
  if (res.error) {
605
776
  console.error('Error:', res.error);
606
777
  process.exit(1);
607
778
  }
608
- console.log(`✓ Mouse moved to (${x},${y}) (${res.duration_ms}ms)`);
779
+ console.log(`✓ Mouse moved to (${res.x},${res.y}) steps=${res.steps ?? 1} (${res.duration_ms}ms)`);
609
780
  });
610
781
  program
611
782
  .command('mouse-down <session-id>')
@@ -745,6 +916,7 @@ function actionCommands(program) {
745
916
  .option('--max-scrolls <n>', 'Maximum scroll steps', '50')
746
917
  .option('--scroll-delta <px>', 'Pixels per scroll step', '300')
747
918
  .option('--stall-ms <ms>', 'Stop if page height unchanged for this many ms', '1500')
919
+ .option('--step-delay-ms <ms>', 'Delay in ms between each scroll step', '0')
748
920
  .action(async (sessionId, opts) => {
749
921
  const body = {
750
922
  direction: opts.direction,
@@ -752,6 +924,9 @@ function actionCommands(program) {
752
924
  scroll_delta: parseInt(opts.scrollDelta),
753
925
  stall_ms: parseInt(opts.stallMs),
754
926
  };
927
+ const stepDelay = parseInt(opts.stepDelayMs ?? '0');
928
+ if (stepDelay > 0)
929
+ body.step_delay_ms = stepDelay;
755
930
  if (opts.scrollSelector)
756
931
  body.scroll_selector = opts.scrollSelector;
757
932
  if (opts.stopSelector)
@@ -983,10 +1158,13 @@ function actionCommands(program) {
983
1158
  // ---------------------------------------------------------------------------
984
1159
  program
985
1160
  .command('bbox <session-id> <selector-or-eid>')
986
- .description('Return the bounding box of an element (selector or element_id)')
1161
+ .description('Return the bounding box of an element (selector, element_id, or ref_id)')
987
1162
  .option('--element-id', 'Treat arg as element_id from element-map')
1163
+ .option('--ref-id', 'Treat arg as a snapshot ref_id (snap_XXXXXX:eN)')
988
1164
  .action(async (sessionId, target, opts) => {
989
- const body = opts.elementId ? { element_id: target } : { selector: target };
1165
+ const body = opts.refId
1166
+ ? { ref_id: target }
1167
+ : opts.elementId ? { element_id: target } : { selector: target };
990
1168
  const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/bbox`, body);
991
1169
  if (res.error) {
992
1170
  console.error('Error:', res.error);
@@ -1097,5 +1275,89 @@ function actionCommands(program) {
1097
1275
  const r = await (0, client_1.apiDelete)(`/api/v1/sessions/${sessionId}/network_conditions`);
1098
1276
  console.log(`✓ Network conditions reset (status ${r.statusCode})`);
1099
1277
  });
1278
+ // ---------------------------------------------------------------------------
1279
+ // r08-c07 P2 — CLI 新命令补齐
1280
+ // ---------------------------------------------------------------------------
1281
+ program
1282
+ .command('find <session-id> <query-type> <query>')
1283
+ .description('Semantic element find: query-type is role|text|label|placeholder|alt_text')
1284
+ .option('--nth <n>', 'Zero-based index when multiple matches exist', '0')
1285
+ .option('--json', 'Output raw JSON')
1286
+ .action(async (sessionId, queryType, query, opts) => {
1287
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/find`, {
1288
+ query_type: queryType,
1289
+ query,
1290
+ nth: parseInt(opts.nth ?? '0'),
1291
+ });
1292
+ if (res.error) {
1293
+ console.error('Error:', res.error);
1294
+ process.exit(1);
1295
+ }
1296
+ if (opts.json) {
1297
+ console.log(JSON.stringify(res, null, 2));
1298
+ return;
1299
+ }
1300
+ if (!res.found) {
1301
+ console.log(`(not found: ${queryType}="${query}")`);
1302
+ return;
1303
+ }
1304
+ const b = res.bbox;
1305
+ console.log(`✓ Found: tag=${res.tag} text="${res.text ?? ''}" query_type=${res.query_type}${b ? ` bbox=(${b.x},${b.y} ${b.width}×${b.height})` : ''} (${res.duration_ms}ms)`);
1306
+ });
1307
+ program
1308
+ .command('settings <session-id>')
1309
+ .description('Get current browser settings for a session (viewport, UA, url, headless, profile)')
1310
+ .option('--json', 'Output raw JSON')
1311
+ .action(async (sessionId, opts) => {
1312
+ const res = await (0, client_1.apiGet)(`/api/v1/sessions/${sessionId}/settings`);
1313
+ if (res.error) {
1314
+ console.error('Error:', res.error);
1315
+ process.exit(1);
1316
+ }
1317
+ if (opts.json) {
1318
+ console.log(JSON.stringify(res, null, 2));
1319
+ return;
1320
+ }
1321
+ const vp = res.viewport ?? {};
1322
+ console.log(`Session: ${res.session_id}`);
1323
+ console.log(` Viewport: ${vp.width ?? '?'}×${vp.height ?? '?'}`);
1324
+ console.log(` URL: ${res.url ?? '(none)'}`);
1325
+ console.log(` Headless: ${res.headless}`);
1326
+ console.log(` Profile: ${res.profile}`);
1327
+ console.log(` UA: ${res.user_agent ?? '(default)'}`);
1328
+ });
1329
+ program
1330
+ .command('cookie-delete <session-id> <name>')
1331
+ .description('Delete a cookie by name for a session')
1332
+ .option('--domain <domain>', 'Restrict deletion to this domain')
1333
+ .action(async (sessionId, name, opts) => {
1334
+ const body = { name };
1335
+ if (opts.domain)
1336
+ body.domain = opts.domain;
1337
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/cookies/delete`, body);
1338
+ if (res.error) {
1339
+ console.error('Error:', res.error);
1340
+ process.exit(1);
1341
+ }
1342
+ console.log(`✓ Deleted ${res.deleted_count} cookie(s) named "${name}" (${res.duration_ms}ms)`);
1343
+ });
1344
+ program
1345
+ .command('upload-url <session-id> <url> <selector-or-eid>')
1346
+ .description('Fetch an asset from URL and upload it to a file input element')
1347
+ .option('--element-id', 'Treat selector-or-eid as element_id from element-map')
1348
+ .option('--ref-id', 'Treat selector-or-eid as snapshot ref_id (snap_XXXXXX:eN)')
1349
+ .action(async (sessionId, url, selectorOrEid, opts) => {
1350
+ const body = opts.refId
1351
+ ? { url, ref_id: selectorOrEid }
1352
+ : opts.elementId
1353
+ ? { url, element_id: selectorOrEid }
1354
+ : { url, selector: selectorOrEid };
1355
+ const res = await (0, client_1.apiPost)(`/api/v1/sessions/${sessionId}/upload_url`, body);
1356
+ if (res.error) {
1357
+ console.error('Error:', res.error);
1358
+ process.exit(1);
1359
+ }
1360
+ console.log(`✓ Uploaded from URL: ${res.filename} (${res.size_bytes}B, ${res.mime_type ?? 'unknown'}, ${res.duration_ms}ms)`);
1361
+ });
1100
1362
  }
1101
1363
  //# sourceMappingURL=actions.js.map