@vosjs/cli 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -143,7 +143,7 @@ A re-record replaces the footage, the cursor track, the frames and everything de
143
143
  }
144
144
  ```
145
145
 
146
- Seven verbs: `wait`, `hover` (`ms` 700), `click`, `type` (`delayMs` 40 per key; `focus: false` skips the focusing click, for a submitting Enter), `scroll`, `move`, `drag` (press, move, release: a range input, a canvas element, a timeline clip). Every step takes an optional unique `id`; give steps ids so a span anchored to a step survives script edits and `vos plan --reuse` can follow it, and so a layer can name the element a step touched (`overlays[].pin.step`; the recorder keeps each hover, click, type and drag step's element rect on `meta.steps[].rect`). Because the CLI issues every input itself, the cursor track is synthesized with exact coordinates, exact timing and fresh element rects, which is what powers element-aware auto-zoom and click effects downstream. The schema is [`schema/actions.schema.json`](./schema/actions.schema.json); `vos validate actions.json` checks a script without running anything.
146
+ Seven verbs: `wait`, `hover` (`ms` 700, the dwell), `click` (`ms` the settle after the press, 150), `type` (`delayMs` 40 per key, paced by the clock; `focus: false` skips the focusing click, for a submitting Enter; `ms` the settle after, 150), `scroll` (`ms` the settle, 200), `move`, `drag` (press, move, release: a range input, a canvas element, a timeline clip; `ms` the travel, 700). The recorder's pace is the script's: a gesture is driven by the clock, so a page that answers each pointer sample slowly costs samples, never seconds, and the only pauses are the ones the script names (`wait`, a hover's dwell, a settle). `vos record` ends with a pace line, what the script asked, what the gestures added, what the page cost, and the steps that ran slow, and the `--json` done event carries it as `pace`. Every step takes an optional unique `id`; give steps ids so a span anchored to a step survives script edits and `vos plan --reuse` can follow it, and so a layer can name the element a step touched (`overlays[].pin.step`; the recorder keeps each hover, click, type and drag step's element rect on `meta.steps[].rect`). Because the CLI issues every input itself, the cursor track is synthesized with exact coordinates, exact timing and fresh element rects, which is what powers element-aware auto-zoom and click effects downstream. The schema is [`schema/actions.schema.json`](./schema/actions.schema.json); `vos validate actions.json` checks a script without running anything.
147
147
 
148
148
  Verified the flow in agent-browser already? `vos actions from-agent-browser steps.jsonl [--out actions.json] [--url] [--viewport WxH]` writes the script from that walk (each command kept beside its `--json` result, the batch record shape; refs resolve through the last `snapshot -i`), and names every step the recorder cannot follow rather than dropping it.
149
149
 
@@ -156,6 +156,8 @@ function validateActions(value) {
156
156
  errors.push(`${at2}: ${s.do} needs a selector`);
157
157
  if (s.do === "wait" && typeof s.ms !== "number")
158
158
  errors.push(`${at2}: wait needs ms`);
159
+ if (s.ms !== void 0 && (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 0))
160
+ errors.push(`${at2}: ms must be a number of milliseconds \u2265 0`);
159
161
  if (s.do === "type" && typeof s.text !== "string")
160
162
  errors.push(`${at2}: type needs text`);
161
163
  if (s.do === "type" && s.focus !== void 0 && typeof s.focus !== "boolean")
@@ -6120,9 +6122,99 @@ function cappedLine(capSeconds) {
6120
6122
  return `stopped at ${formatDurationCap(capSeconds)} (--max-duration ${capSeconds}); the remaining steps did not run`;
6121
6123
  }
6122
6124
 
6125
+ // src/plugin/pace.ts
6126
+ var POINTER_MS_PER_PX = 0.7;
6127
+ var POINTER_MIN_MS = 250;
6128
+ var POINTER_MAX_MS = 800;
6129
+ var MOTION_TICK_MS = 16;
6130
+ var SETTLE_MS = {
6131
+ click: 150,
6132
+ type: 150,
6133
+ scroll: 200,
6134
+ drag: 80
6135
+ };
6136
+ var PRESS_LEAD_MS = 80;
6137
+ var PRESS_HOLD_MS = 70;
6138
+ var SCROLL_SETTLE_MS = 120;
6139
+ var TRAILING_HOLD_MS = 400;
6140
+ function pointerTravelMs(dist) {
6141
+ return Math.min(
6142
+ POINTER_MAX_MS,
6143
+ Math.max(POINTER_MIN_MS, Math.round(dist * POINTER_MS_PER_PX))
6144
+ );
6145
+ }
6146
+ function settleMs(step) {
6147
+ if (typeof step.ms === "number" && step.ms >= 0) return step.ms;
6148
+ const d = step.do;
6149
+ return SETTLE_MS[d] ?? 0;
6150
+ }
6151
+ var easeInOutCubic = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
6152
+ async function clockMotion(dur, at2, clock) {
6153
+ const start = clock.now();
6154
+ let samples = 0;
6155
+ for (; ; ) {
6156
+ const elapsed = clock.now() - start;
6157
+ if (elapsed >= dur) break;
6158
+ await at2(easeInOutCubic(Math.min(1, elapsed / dur)));
6159
+ samples++;
6160
+ const next = start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS;
6161
+ const wait = Math.min(next, start + dur) - clock.now();
6162
+ if (wait > 0) await clock.sleep(wait);
6163
+ }
6164
+ await at2(1);
6165
+ return samples + 1;
6166
+ }
6167
+ async function clockTyping(chars, delay, type, clock) {
6168
+ const start = clock.now();
6169
+ for (let i = 0; i < chars.length; i++) {
6170
+ await type(chars[i], i);
6171
+ const due = start + (i + 1) * delay;
6172
+ const wait = due - clock.now();
6173
+ if (wait > 0 && i < chars.length - 1) await clock.sleep(wait);
6174
+ }
6175
+ }
6176
+ function askedMs(step) {
6177
+ switch (step.do) {
6178
+ case "wait":
6179
+ return step.ms ?? 0;
6180
+ case "hover":
6181
+ return step.ms ?? 700;
6182
+ case "drag":
6183
+ return step.ms ?? 700;
6184
+ case "type":
6185
+ return (step.text?.length ?? 0) * (step.delayMs ?? 40);
6186
+ default:
6187
+ return 0;
6188
+ }
6189
+ }
6190
+ function paceReport(steps) {
6191
+ const askedMs2 = steps.reduce((a, s) => a + s.askedMs, 0);
6192
+ const gestureMs = steps.reduce((a, s) => a + s.gestureMs, 0);
6193
+ const wallMs = steps.reduce((a, s) => a + s.wallMs, 0);
6194
+ const overheadMs = Math.max(0, wallMs - askedMs2 - gestureMs);
6195
+ const slow = steps.filter((s) => s.wallMs > (s.askedMs + s.gestureMs) * 1.34 + 60).map((s) => ({
6196
+ step: s.step,
6197
+ do: s.do,
6198
+ askedMs: s.askedMs,
6199
+ wallMs: s.wallMs
6200
+ }));
6201
+ return {
6202
+ askedMs: askedMs2,
6203
+ gestureMs,
6204
+ wallMs,
6205
+ overheadMs,
6206
+ overheadPct: wallMs > 0 ? Math.round(overheadMs / wallMs * 100) : 0,
6207
+ slow
6208
+ };
6209
+ }
6210
+ function paceLine(r) {
6211
+ const s = (ms) => `${(ms / 1e3).toFixed(1)} s`;
6212
+ const slow = r.slow.length ? `; slow: ${r.slow.map((x) => `#${x.step} ${x.do} ${s(x.wallMs)} for ${s(x.askedMs)} asked`).join(", ")}` : "";
6213
+ return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`;
6214
+ }
6215
+
6123
6216
  // src/plugin/recorder.ts
6124
6217
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6125
- var easeInOutCubic = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
6126
6218
  function jpegDims(buf) {
6127
6219
  let i = 2;
6128
6220
  while (i < buf.length - 9) {
@@ -6181,26 +6273,30 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6181
6273
  const emit = (e) => events.push({ t: now(), ...e });
6182
6274
  await page.mouse.move(cur.x, cur.y);
6183
6275
  emit({ x: cur.x, y: cur.y, type: "move" });
6276
+ const clock = { now: () => Date.now(), sleep };
6184
6277
  const moveTo = async (tx, ty) => {
6185
6278
  const dist = Math.hypot(tx - cur.x, ty - cur.y);
6186
- const dur = Math.min(1200, Math.max(350, dist * 1.4));
6187
- const steps = Math.max(6, Math.round(dur / 16));
6188
6279
  const from = { ...cur };
6189
- for (let i = 1; i <= steps; i++) {
6190
- const u = easeInOutCubic(i / steps);
6191
- cur.x = from.x + (tx - from.x) * u;
6192
- cur.y = from.y + (ty - from.y) * u;
6193
- await page.mouse.move(cur.x, cur.y);
6194
- emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
6195
- await sleep(14);
6196
- }
6280
+ await clockMotion(
6281
+ pointerTravelMs(dist),
6282
+ async (u) => {
6283
+ cur.x = from.x + (tx - from.x) * u;
6284
+ cur.y = from.y + (ty - from.y) * u;
6285
+ await page.mouse.move(cur.x, cur.y);
6286
+ emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
6287
+ },
6288
+ clock
6289
+ );
6197
6290
  };
6198
6291
  const boxOf = async (selector) => {
6199
6292
  const loc = page.locator(selector).first();
6200
6293
  try {
6201
6294
  await loc.waitFor({ state: "visible", timeout: 8e3 });
6295
+ const before = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => [0, 0]);
6202
6296
  await loc.scrollIntoViewIfNeeded();
6203
- await sleep(250);
6297
+ const after = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => before);
6298
+ if (before[0] !== after[0] || before[1] !== after[1])
6299
+ await sleep(SCROLL_SETTLE_MS);
6204
6300
  const bb = await loc.boundingBox();
6205
6301
  if (!bb) return null;
6206
6302
  return { x: bb.x, y: bb.y, w: bb.width, h: bb.height };
@@ -6211,7 +6307,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6211
6307
  };
6212
6308
  const clickAt = async (rect) => {
6213
6309
  await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
6214
- await sleep(120);
6310
+ await sleep(PRESS_LEAD_MS);
6215
6311
  emit({
6216
6312
  x: Math.round(cur.x),
6217
6313
  y: Math.round(cur.y),
@@ -6220,7 +6316,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6220
6316
  rect
6221
6317
  });
6222
6318
  await page.mouse.down();
6223
- await sleep(90);
6319
+ await sleep(PRESS_HOLD_MS);
6224
6320
  await page.mouse.up();
6225
6321
  emit({
6226
6322
  x: Math.round(cur.x),
@@ -6232,6 +6328,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6232
6328
  };
6233
6329
  const skipped = [];
6234
6330
  const stepSpans = [];
6331
+ const paces = [];
6235
6332
  let capped = false;
6236
6333
  for (const [stepIdx, step] of actions.steps.entries()) {
6237
6334
  if (capReached(now(), maxSeconds)) {
@@ -6239,6 +6336,13 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6239
6336
  break;
6240
6337
  }
6241
6338
  const stepStart = now();
6339
+ let gestureMs = 0;
6340
+ const gesture = async (fn) => {
6341
+ const t = now();
6342
+ const out = await fn();
6343
+ gestureMs += now() - t;
6344
+ return out;
6345
+ };
6242
6346
  const skippedBefore = skipped.length;
6243
6347
  const urlBefore = page.url();
6244
6348
  let stepRect = null;
@@ -6253,7 +6357,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6253
6357
  const rect = await boxOf(step.selector);
6254
6358
  stepRect = rect;
6255
6359
  if (rect) {
6256
- await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
6360
+ await gesture(() => moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2));
6257
6361
  log(`hover ${step.selector}`);
6258
6362
  await sleep(step.ms ?? 700);
6259
6363
  } else
@@ -6264,9 +6368,9 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6264
6368
  const rect = await boxOf(step.selector);
6265
6369
  stepRect = rect;
6266
6370
  if (rect) {
6267
- await clickAt(rect);
6371
+ await gesture(() => clickAt(rect));
6268
6372
  log(`click ${step.selector}`);
6269
- await sleep(500);
6373
+ await gesture(() => sleep(settleMs(step)));
6270
6374
  } else
6271
6375
  skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
6272
6376
  break;
@@ -6275,7 +6379,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6275
6379
  const rect = await boxOf(step.selector);
6276
6380
  stepRect = rect;
6277
6381
  if (rect) {
6278
- if (step.focus !== false) await clickAt(rect);
6382
+ if (step.focus !== false) await gesture(() => clickAt(rect));
6279
6383
  const delay = step.delayMs ?? 40;
6280
6384
  const ping = () => emit({
6281
6385
  x: Math.round(rect.x + rect.w / 2),
@@ -6284,17 +6388,21 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6284
6388
  rect
6285
6389
  });
6286
6390
  let lastPing = -Infinity;
6287
- for (const ch of step.text) {
6288
- if (now() - lastPing >= 350) {
6289
- lastPing = now();
6290
- ping();
6291
- }
6292
- await page.keyboard.type(ch);
6293
- await sleep(delay);
6294
- }
6391
+ await clockTyping(
6392
+ [...step.text],
6393
+ delay,
6394
+ async (ch) => {
6395
+ if (now() - lastPing >= 350) {
6396
+ lastPing = now();
6397
+ ping();
6398
+ }
6399
+ await page.keyboard.type(ch);
6400
+ },
6401
+ clock
6402
+ );
6295
6403
  ping();
6296
6404
  log(`type "${step.text}" into ${step.selector}`);
6297
- await sleep(300);
6405
+ await gesture(() => sleep(settleMs(step)));
6298
6406
  } else
6299
6407
  skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
6300
6408
  break;
@@ -6307,7 +6415,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6307
6415
  emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "scroll" });
6308
6416
  await sleep(40);
6309
6417
  }
6310
- await sleep(400);
6418
+ await gesture(() => sleep(settleMs(step)));
6311
6419
  break;
6312
6420
  }
6313
6421
  case "drag": {
@@ -6331,8 +6439,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6331
6439
  start = { x: step.x, y: step.y };
6332
6440
  }
6333
6441
  if (!start) break;
6334
- await moveTo(start.x, start.y);
6335
- await sleep(140);
6442
+ await gesture(() => moveTo(start.x, start.y));
6443
+ await gesture(() => sleep(PRESS_LEAD_MS));
6336
6444
  emit({
6337
6445
  x: Math.round(cur.x),
6338
6446
  y: Math.round(cur.y),
@@ -6341,19 +6449,20 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6341
6449
  rect
6342
6450
  });
6343
6451
  await page.mouse.down();
6344
- await sleep(90);
6452
+ await gesture(() => sleep(PRESS_HOLD_MS));
6345
6453
  const dur = step.ms ?? 700;
6346
- const steps = Math.max(8, Math.round(dur / 16));
6347
6454
  const from = { ...cur };
6348
- for (let i = 1; i <= steps; i++) {
6349
- const u = easeInOutCubic(i / steps);
6350
- cur.x = from.x + (step.tx - from.x) * u;
6351
- cur.y = from.y + (step.ty - from.y) * u;
6352
- await page.mouse.move(cur.x, cur.y);
6353
- emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
6354
- await sleep(14);
6355
- }
6356
- await sleep(100);
6455
+ await clockMotion(
6456
+ dur,
6457
+ async (u) => {
6458
+ cur.x = from.x + (step.tx - from.x) * u;
6459
+ cur.y = from.y + (step.ty - from.y) * u;
6460
+ await page.mouse.move(cur.x, cur.y);
6461
+ emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
6462
+ },
6463
+ clock
6464
+ );
6465
+ await gesture(() => sleep(settleMs({ do: "drag" })));
6357
6466
  await page.mouse.up();
6358
6467
  emit({
6359
6468
  x: Math.round(cur.x),
@@ -6388,8 +6497,15 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6388
6497
  }
6389
6498
  } : {}
6390
6499
  });
6500
+ paces.push({
6501
+ step: stepIdx,
6502
+ do: step.do,
6503
+ askedMs: askedMs(step),
6504
+ gestureMs,
6505
+ wallMs: now() - stepStart
6506
+ });
6391
6507
  }
6392
- if (!capped) await sleep(600);
6508
+ if (!capped) await sleep(TRAILING_HOLD_MS);
6393
6509
  if (capped || capReached(now(), maxSeconds)) {
6394
6510
  capped = true;
6395
6511
  log(` ${cappedLine(maxSeconds)}`);
@@ -6441,10 +6557,13 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6441
6557
  await writeJson(paths.meta, meta, true);
6442
6558
  await writeJson(paths.framesIndex, frames);
6443
6559
  await writeJson(paths.actions, { ...actions, url }, true);
6560
+ const pace = paceReport(paces);
6561
+ log(` ${paceLine(pace)}`);
6444
6562
  return {
6445
6563
  events,
6446
6564
  frames,
6447
6565
  meta,
6566
+ pace,
6448
6567
  skipped,
6449
6568
  navTimeout,
6450
6569
  freezes,
@@ -9944,6 +10063,7 @@ async function cmdRecord(argv) {
9944
10063
  freezes: rec.freezes,
9945
10064
  freezePct: rec.freezePct,
9946
10065
  capped: rec.capped,
10066
+ pace: rec.pace,
9947
10067
  ...strictFail ? { strictFailed: true } : {}
9948
10068
  },
9949
10069
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
@@ -11119,4 +11239,4 @@ export {
11119
11239
  convertAgentBrowser,
11120
11240
  run
11121
11241
  };
11122
- //# sourceMappingURL=chunk-6O55OD6D.js.map
11242
+ //# sourceMappingURL=chunk-43FCXWNS.js.map