@vosjs/cli 0.33.0 → 0.35.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
 
@@ -153,7 +153,7 @@ Verified the flow in agent-browser already? `vos actions from-agent-browser step
153
153
 
154
154
  **Contracts that bite.** Time is source seconds in `zoom`, `segments`, `speed` and `tilt`, and output seconds in `overlays` and `audio`. Zoom `cx`/`cy`, overlay `transform.x`/`y` and a pin's `rect` are normalized fractions of the frame in `[0, 1]` (`0.5, 0.5` is the centre), never pixels. `level` is 1..5. The planners write `source: "auto"`; spans you add or edit carry `source: "manual"`, which a re-plan never touches, and a deleted automatic span is recorded in `doc.rejected` so a re-plan does not bring it back.
155
155
 
156
- **Camera styles.** `doc.zoomStyle` is one of `glide` (default), `focus`, `cinema`, `snappy`, `cut`, `keynote` (gliding zooms plus a medium lean toward each focus, the launch-film register), `drift` (slow ambient zooms with a subtle lean) or `none`. In the studio, picking a style also stamps `tiltStyle` and plans the tilt spans; in `doc.json` set `tiltStyle` and `tilt` yourself.
156
+ **Camera styles.** `doc.zoomStyle` is one of `glide` (default: one steady zoom that travels between clicks), `focus` (a zoom per click, framed and released), `cinema` (slow moves and long holds), `snappy` (quick cycles), `cut` (instant zoom-ins) or `none`. Every style times the tilt track to its own zoom tempo, so a lean lands with its zoom; how far the card leans is `tiltStyle` (`off` | `subtle` | `medium` | `strong`), which the studio stamps when a style is picked and `doc.json` sets itself, with `tilt` spans. `keynote` and `drift` are retired names that still read: `glide` + `tiltStyle: medium` and `cinema` + `tiltStyle: subtle`.
157
157
 
158
158
  **Tilt.** `doc.tilt`: source-anchored, non-overlapping spans where the card leans to a pose and returns to rest: `[{ "id": "u0", "in": 4, "out": 8, "rx": 6, "ry": -9, "source": "manual" }]`. `rx`/`ry` are degrees (±45 hard limit; ±5..18 reads well); `+rx` brings the top edge toward the camera, `+ry` the left edge, so a lean toward a right-side focus is a negative `ry`. Rest is flat. `doc.tiltStyle` (`off`, `subtle`, `medium`, `strong`) is the intensity the planner derives automatic spans from.
159
159
 
@@ -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")
@@ -205,6 +207,7 @@ import {
205
207
  TEXT_ENTER_KINDS,
206
208
  TEXT_EXIT_KINDS,
207
209
  TILT_DEG_MAX,
210
+ RETIRED_ZOOM_STYLES,
208
211
  TILT_SPAN_MIN,
209
212
  ZOOM_LEVEL_MAX,
210
213
  ZOOM_LEVEL_MIN,
@@ -664,6 +667,12 @@ function lintDoc(docIn) {
664
667
  );
665
668
  }
666
669
  }
670
+ if (typeof doc.zoomStyle === "string" && doc.zoomStyle in RETIRED_ZOOM_STYLES) {
671
+ const r = RETIRED_ZOOM_STYLES[doc.zoomStyle];
672
+ warnings.push(
673
+ `zoomStyle "${doc.zoomStyle}" is retired: it reads as "${r.style}" with tiltStyle "${r.tilt}" (write those instead)`
674
+ );
675
+ }
667
676
  if (doc.tiltStyle !== void 0 && !["off", "subtle", "medium", "strong"].includes(doc.tiltStyle)) {
668
677
  problems.push(
669
678
  `tiltStyle must be "off" | "subtle" | "medium" | "strong" (got ${String(doc.tiltStyle)})`
@@ -6120,9 +6129,99 @@ function cappedLine(capSeconds) {
6120
6129
  return `stopped at ${formatDurationCap(capSeconds)} (--max-duration ${capSeconds}); the remaining steps did not run`;
6121
6130
  }
6122
6131
 
6132
+ // src/plugin/pace.ts
6133
+ var POINTER_MS_PER_PX = 0.7;
6134
+ var POINTER_MIN_MS = 250;
6135
+ var POINTER_MAX_MS = 800;
6136
+ var MOTION_TICK_MS = 16;
6137
+ var SETTLE_MS = {
6138
+ click: 150,
6139
+ type: 150,
6140
+ scroll: 200,
6141
+ drag: 80
6142
+ };
6143
+ var PRESS_LEAD_MS = 80;
6144
+ var PRESS_HOLD_MS = 70;
6145
+ var SCROLL_SETTLE_MS = 120;
6146
+ var TRAILING_HOLD_MS = 400;
6147
+ function pointerTravelMs(dist) {
6148
+ return Math.min(
6149
+ POINTER_MAX_MS,
6150
+ Math.max(POINTER_MIN_MS, Math.round(dist * POINTER_MS_PER_PX))
6151
+ );
6152
+ }
6153
+ function settleMs(step) {
6154
+ if (typeof step.ms === "number" && step.ms >= 0) return step.ms;
6155
+ const d = step.do;
6156
+ return SETTLE_MS[d] ?? 0;
6157
+ }
6158
+ var easeInOutCubic = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
6159
+ async function clockMotion(dur, at2, clock) {
6160
+ const start = clock.now();
6161
+ let samples = 0;
6162
+ for (; ; ) {
6163
+ const elapsed = clock.now() - start;
6164
+ if (elapsed >= dur) break;
6165
+ await at2(easeInOutCubic(Math.min(1, elapsed / dur)));
6166
+ samples++;
6167
+ const next = start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS;
6168
+ const wait = Math.min(next, start + dur) - clock.now();
6169
+ if (wait > 0) await clock.sleep(wait);
6170
+ }
6171
+ await at2(1);
6172
+ return samples + 1;
6173
+ }
6174
+ async function clockTyping(chars, delay, type, clock) {
6175
+ const start = clock.now();
6176
+ for (let i = 0; i < chars.length; i++) {
6177
+ await type(chars[i], i);
6178
+ const due = start + (i + 1) * delay;
6179
+ const wait = due - clock.now();
6180
+ if (wait > 0 && i < chars.length - 1) await clock.sleep(wait);
6181
+ }
6182
+ }
6183
+ function askedMs(step) {
6184
+ switch (step.do) {
6185
+ case "wait":
6186
+ return step.ms ?? 0;
6187
+ case "hover":
6188
+ return step.ms ?? 700;
6189
+ case "drag":
6190
+ return step.ms ?? 700;
6191
+ case "type":
6192
+ return (step.text?.length ?? 0) * (step.delayMs ?? 40);
6193
+ default:
6194
+ return 0;
6195
+ }
6196
+ }
6197
+ function paceReport(steps) {
6198
+ const askedMs2 = steps.reduce((a, s) => a + s.askedMs, 0);
6199
+ const gestureMs = steps.reduce((a, s) => a + s.gestureMs, 0);
6200
+ const wallMs = steps.reduce((a, s) => a + s.wallMs, 0);
6201
+ const overheadMs = Math.max(0, wallMs - askedMs2 - gestureMs);
6202
+ const slow = steps.filter((s) => s.wallMs > (s.askedMs + s.gestureMs) * 1.34 + 60).map((s) => ({
6203
+ step: s.step,
6204
+ do: s.do,
6205
+ askedMs: s.askedMs,
6206
+ wallMs: s.wallMs
6207
+ }));
6208
+ return {
6209
+ askedMs: askedMs2,
6210
+ gestureMs,
6211
+ wallMs,
6212
+ overheadMs,
6213
+ overheadPct: wallMs > 0 ? Math.round(overheadMs / wallMs * 100) : 0,
6214
+ slow
6215
+ };
6216
+ }
6217
+ function paceLine(r) {
6218
+ const s = (ms) => `${(ms / 1e3).toFixed(1)} s`;
6219
+ const slow = r.slow.length ? `; slow: ${r.slow.map((x) => `#${x.step} ${x.do} ${s(x.wallMs)} for ${s(x.askedMs)} asked`).join(", ")}` : "";
6220
+ return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`;
6221
+ }
6222
+
6123
6223
  // src/plugin/recorder.ts
6124
6224
  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
6225
  function jpegDims(buf) {
6127
6226
  let i = 2;
6128
6227
  while (i < buf.length - 9) {
@@ -6181,26 +6280,30 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6181
6280
  const emit = (e) => events.push({ t: now(), ...e });
6182
6281
  await page.mouse.move(cur.x, cur.y);
6183
6282
  emit({ x: cur.x, y: cur.y, type: "move" });
6283
+ const clock = { now: () => Date.now(), sleep };
6184
6284
  const moveTo = async (tx, ty) => {
6185
6285
  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
6286
  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
- }
6287
+ await clockMotion(
6288
+ pointerTravelMs(dist),
6289
+ async (u) => {
6290
+ cur.x = from.x + (tx - from.x) * u;
6291
+ cur.y = from.y + (ty - from.y) * u;
6292
+ await page.mouse.move(cur.x, cur.y);
6293
+ emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
6294
+ },
6295
+ clock
6296
+ );
6197
6297
  };
6198
6298
  const boxOf = async (selector) => {
6199
6299
  const loc = page.locator(selector).first();
6200
6300
  try {
6201
6301
  await loc.waitFor({ state: "visible", timeout: 8e3 });
6302
+ const before = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => [0, 0]);
6202
6303
  await loc.scrollIntoViewIfNeeded();
6203
- await sleep(250);
6304
+ const after = await page.evaluate(() => [window.scrollX, window.scrollY]).catch(() => before);
6305
+ if (before[0] !== after[0] || before[1] !== after[1])
6306
+ await sleep(SCROLL_SETTLE_MS);
6204
6307
  const bb = await loc.boundingBox();
6205
6308
  if (!bb) return null;
6206
6309
  return { x: bb.x, y: bb.y, w: bb.width, h: bb.height };
@@ -6211,7 +6314,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6211
6314
  };
6212
6315
  const clickAt = async (rect) => {
6213
6316
  await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
6214
- await sleep(120);
6317
+ await sleep(PRESS_LEAD_MS);
6215
6318
  emit({
6216
6319
  x: Math.round(cur.x),
6217
6320
  y: Math.round(cur.y),
@@ -6220,7 +6323,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6220
6323
  rect
6221
6324
  });
6222
6325
  await page.mouse.down();
6223
- await sleep(90);
6326
+ await sleep(PRESS_HOLD_MS);
6224
6327
  await page.mouse.up();
6225
6328
  emit({
6226
6329
  x: Math.round(cur.x),
@@ -6232,6 +6335,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6232
6335
  };
6233
6336
  const skipped = [];
6234
6337
  const stepSpans = [];
6338
+ const paces = [];
6235
6339
  let capped = false;
6236
6340
  for (const [stepIdx, step] of actions.steps.entries()) {
6237
6341
  if (capReached(now(), maxSeconds)) {
@@ -6239,6 +6343,13 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6239
6343
  break;
6240
6344
  }
6241
6345
  const stepStart = now();
6346
+ let gestureMs = 0;
6347
+ const gesture = async (fn) => {
6348
+ const t = now();
6349
+ const out = await fn();
6350
+ gestureMs += now() - t;
6351
+ return out;
6352
+ };
6242
6353
  const skippedBefore = skipped.length;
6243
6354
  const urlBefore = page.url();
6244
6355
  let stepRect = null;
@@ -6253,7 +6364,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6253
6364
  const rect = await boxOf(step.selector);
6254
6365
  stepRect = rect;
6255
6366
  if (rect) {
6256
- await moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2);
6367
+ await gesture(() => moveTo(rect.x + rect.w / 2, rect.y + rect.h / 2));
6257
6368
  log(`hover ${step.selector}`);
6258
6369
  await sleep(step.ms ?? 700);
6259
6370
  } else
@@ -6264,9 +6375,9 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6264
6375
  const rect = await boxOf(step.selector);
6265
6376
  stepRect = rect;
6266
6377
  if (rect) {
6267
- await clickAt(rect);
6378
+ await gesture(() => clickAt(rect));
6268
6379
  log(`click ${step.selector}`);
6269
- await sleep(500);
6380
+ await gesture(() => sleep(settleMs(step)));
6270
6381
  } else
6271
6382
  skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
6272
6383
  break;
@@ -6275,7 +6386,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6275
6386
  const rect = await boxOf(step.selector);
6276
6387
  stepRect = rect;
6277
6388
  if (rect) {
6278
- if (step.focus !== false) await clickAt(rect);
6389
+ if (step.focus !== false) await gesture(() => clickAt(rect));
6279
6390
  const delay = step.delayMs ?? 40;
6280
6391
  const ping = () => emit({
6281
6392
  x: Math.round(rect.x + rect.w / 2),
@@ -6284,17 +6395,21 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6284
6395
  rect
6285
6396
  });
6286
6397
  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
- }
6398
+ await clockTyping(
6399
+ [...step.text],
6400
+ delay,
6401
+ async (ch) => {
6402
+ if (now() - lastPing >= 350) {
6403
+ lastPing = now();
6404
+ ping();
6405
+ }
6406
+ await page.keyboard.type(ch);
6407
+ },
6408
+ clock
6409
+ );
6295
6410
  ping();
6296
6411
  log(`type "${step.text}" into ${step.selector}`);
6297
- await sleep(300);
6412
+ await gesture(() => sleep(settleMs(step)));
6298
6413
  } else
6299
6414
  skipped.push({ step: stepIdx, do: step.do, selector: step.selector });
6300
6415
  break;
@@ -6307,7 +6422,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6307
6422
  emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "scroll" });
6308
6423
  await sleep(40);
6309
6424
  }
6310
- await sleep(400);
6425
+ await gesture(() => sleep(settleMs(step)));
6311
6426
  break;
6312
6427
  }
6313
6428
  case "drag": {
@@ -6331,8 +6446,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6331
6446
  start = { x: step.x, y: step.y };
6332
6447
  }
6333
6448
  if (!start) break;
6334
- await moveTo(start.x, start.y);
6335
- await sleep(140);
6449
+ await gesture(() => moveTo(start.x, start.y));
6450
+ await gesture(() => sleep(PRESS_LEAD_MS));
6336
6451
  emit({
6337
6452
  x: Math.round(cur.x),
6338
6453
  y: Math.round(cur.y),
@@ -6341,19 +6456,20 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6341
6456
  rect
6342
6457
  });
6343
6458
  await page.mouse.down();
6344
- await sleep(90);
6459
+ await gesture(() => sleep(PRESS_HOLD_MS));
6345
6460
  const dur = step.ms ?? 700;
6346
- const steps = Math.max(8, Math.round(dur / 16));
6347
6461
  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);
6462
+ await clockMotion(
6463
+ dur,
6464
+ async (u) => {
6465
+ cur.x = from.x + (step.tx - from.x) * u;
6466
+ cur.y = from.y + (step.ty - from.y) * u;
6467
+ await page.mouse.move(cur.x, cur.y);
6468
+ emit({ x: Math.round(cur.x), y: Math.round(cur.y), type: "move" });
6469
+ },
6470
+ clock
6471
+ );
6472
+ await gesture(() => sleep(settleMs({ do: "drag" })));
6357
6473
  await page.mouse.up();
6358
6474
  emit({
6359
6475
  x: Math.round(cur.x),
@@ -6388,8 +6504,15 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6388
6504
  }
6389
6505
  } : {}
6390
6506
  });
6507
+ paces.push({
6508
+ step: stepIdx,
6509
+ do: step.do,
6510
+ askedMs: askedMs(step),
6511
+ gestureMs,
6512
+ wallMs: now() - stepStart
6513
+ });
6391
6514
  }
6392
- if (!capped) await sleep(600);
6515
+ if (!capped) await sleep(TRAILING_HOLD_MS);
6393
6516
  if (capped || capReached(now(), maxSeconds)) {
6394
6517
  capped = true;
6395
6518
  log(` ${cappedLine(maxSeconds)}`);
@@ -6441,10 +6564,13 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6441
6564
  await writeJson(paths.meta, meta, true);
6442
6565
  await writeJson(paths.framesIndex, frames);
6443
6566
  await writeJson(paths.actions, { ...actions, url }, true);
6567
+ const pace = paceReport(paces);
6568
+ log(` ${paceLine(pace)}`);
6444
6569
  return {
6445
6570
  events,
6446
6571
  frames,
6447
6572
  meta,
6573
+ pace,
6448
6574
  skipped,
6449
6575
  navTimeout,
6450
6576
  freezes,
@@ -9944,6 +10070,7 @@ async function cmdRecord(argv) {
9944
10070
  freezes: rec.freezes,
9945
10071
  freezePct: rec.freezePct,
9946
10072
  capped: rec.capped,
10073
+ pace: rec.pace,
9947
10074
  ...strictFail ? { strictFailed: true } : {}
9948
10075
  },
9949
10076
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
@@ -11119,4 +11246,4 @@ export {
11119
11246
  convertAgentBrowser,
11120
11247
  run
11121
11248
  };
11122
- //# sourceMappingURL=chunk-6O55OD6D.js.map
11249
+ //# sourceMappingURL=chunk-VQXOM2NU.js.map