@vosjs/cli 0.42.0 → 0.44.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
@@ -80,7 +80,7 @@ vos plan take --reuse # re-time that cut onto
80
80
 
81
81
  | Verb | Flags |
82
82
  | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
- | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--dry-run` `--allow-wall` `--keep-frames` `--storage-state <file>` `--browser-arg=<switch>`... `--max-duration <s>` `--background <slug\|url\|none>` |
83
+ | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--dry-run` `--allow-wall` `--keep-frames` `--storage-state <file>` `--header name=value`... `--browser-arg=<switch>`... `--max-duration <s>` `--background <slug\|url\|none>` |
84
84
  | `create` | The `record` flags plus the render flags (`--width` `--height` `--fps` `--format` `--parallel` `--draft` `--frame` `--set`), no `--range`. With `--strict` an incomplete recording exits 2 before anything is rendered |
85
85
  | `plan` | `--fresh` (discard the current plan) `--reuse` `--from <doc.json>` (defaults to `<take>/doc.prev.json`) `--style <doc.json\|take\|vosId>` `--with <doc.json\|take\|vosId>[@end\|@start\|@step:<id>\|@<seconds>]` (a template, repeatable) `--background` `--motion` (re-propose the motion) `--headline` `--kicker` `--launch` `--brand` `--music` `--entrance` `--transitions slide\|fade\|scale\|none` `--end-card on\|none\|<ref>` `--captions` `--clicks` `--release` |
86
86
  | `digest` | `--out <take>/digest` `--full 960` `--crop 640` (image long edges, the token budget) `--no-frames` `--transcript <file>` (Whisper-shaped segments merged as `said`) `--style <ref>` (report a reference document's style fields) |
@@ -90,6 +90,21 @@ vos plan take --reuse # re-time that cut onto
90
90
 
91
91
  **The wall check.** Once the first navigation settles, and before a frame is captured, `record`, `create` and `--dry-run` ask whether the recorder landed where it was sent. A take that met a sign-in instead is refused with exit 4 and a sentence (`asked for /dashboard, landed on /login: no session for app.acme.com`): the asked URL answered 401 or 403, the recorder was sent to an identity provider or a sign-in path, or the page is a sign-in form (one password field, or a one-time-code field) rendered in place. A redirect somewhere else with no sign-in in sight, which is what a site that shows strangers a public page looks like, is refused under `--strict` and in a rehearsal, and said as a warning otherwise. The check runs before a re-record clears anything, so a session that expired since the last take never costs the footage it failed to replace. The way past a wall is a session: `--storage-state <file>`, minted from the test auth the project already has wherever that exists. `--allow-wall` records the page anyway (a video OF a sign-in page is a legitimate take), and the take's `meta.wall` and its digest then say so.
92
92
 
93
+ **`setup`: the steps that run before the camera rolls.** A sign-in form, a cookie banner, the "choose your editor" modal, an onboarding tour: things a take must get past and must not show. `actions.json` takes `setup: [...]` beside `steps`, with the verbs `goto`, `click`, `type`, `press`, `wait`. They run after the first navigation and before a frame is captured, as plain actions with no cursor, no frames, no pace and nothing in `meta.steps`; then the recorder opens `url` again and the take begins where the setup left it. A `type` step's `text` may be `{ "env": "DEMO_PASSWORD" }`, read from the shell at run time and never logged or stored (the log names the field, never the value; a literal typed into a password field is refused by `validate`, because `actions.json` is committed and pushed with the take). A selector that never appears fails the take before anything is recorded (exit 2), because a take that begins at a half-finished sign-in is the wall by another name; rehearse the setup with `--dry-run` like everything else. This is rung 2 of the session ladder made scriptable: a local or self-hosted instance with a seeded user, no state file needed.
94
+
95
+ ```json
96
+ "setup": [
97
+ { "do": "goto", "url": "http://localhost:3000/login" },
98
+ { "do": "type", "selector": "#email", "text": "demo@acme.test" },
99
+ { "do": "type", "selector": "#password", "text": { "env": "DEMO_PASSWORD" } },
100
+ { "do": "press", "key": "Enter", "ms": 800 }
101
+ ]
102
+ ```
103
+
104
+ **`vos actions script <actions.json>`: the flow as a shot list.** When no rung of the session ladder holds and a person has to record the take themselves (in their own signed-in browser, with the vosso extension), the agent hands over the flow it worked out rather than an apology: numbered beats in plain words with the holds the script asked for, the page to start on and about how long. A step's `caption` leads its beat, a `text=` or `:has-text()` selector is said as its words, and a step's `id` is said as a name (`new-order` reads as "new order"), so give steps ids and captions that a person could follow. `--json` carries the beats beside the text. The take comes back through the ordinary handoff, and the agent cuts it (`vos pull --media`, then the `vos-cut` skill).
105
+
106
+ **`--header name=value`**, repeatable: a request header on every request the recording browser makes, the way past a preview deployment protected by a bypass header alone (`--header x-vercel-protection-bypass=$TOKEN`). The rehearsal's `Next:` line carries it.
107
+
93
108
  **What the frame shows.** Getting past a login puts the account's own data in the picture, and asking for a demo account does not hold that line: a person asked to sign in signs in as themselves. So the recorder looks. After the page opens and after every step it reads the text visible in the viewport and reports the KIND of thing it saw and where, never the string: an email address (one on `example.com` or a `.test`, `.example`, `.invalid` or `.localhost` domain is demo data and is not reported), something shaped like an API key or a JWT, a card number that passes the Luhn check, a masked card's visible tail. They land in `meta.exposures[]` (`step`, `kind`, `selector`, `rect`, `seen`), in the `record` and `create` done events, at the end of a rehearsal (before anything is recorded), as warnings in `vos validate <take>`, and in the digest's `take.exposures`. They warn; they do not fail a take, because a product may legitimately show addresses. The `selector` reaches that element and no other, so it can be pasted into a mask.
94
109
 
95
110
  **`mask`** in `actions.json` hides a selector BEFORE the first frame is captured and keeps it hidden across navigations and re-renders, so the real value is never in a frame, never in the recording, never pushed:
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/plugin/pace.ts
4
+ var POINTER_MS_PER_PX = 0.7;
5
+ var POINTER_MIN_MS = 250;
6
+ var POINTER_MAX_MS = 800;
7
+ var MOTION_TICK_MS = 16;
8
+ var SETTLE_MS = {
9
+ click: 150,
10
+ type: 150,
11
+ scroll: 200,
12
+ drag: 80
13
+ };
14
+ var PRESS_LEAD_MS = 80;
15
+ var PRESS_HOLD_MS = 70;
16
+ var SCROLL_SETTLE_MS = 120;
17
+ var TRAILING_HOLD_MS = 400;
18
+ function pointerTravelMs(dist) {
19
+ return Math.min(
20
+ POINTER_MAX_MS,
21
+ Math.max(POINTER_MIN_MS, Math.round(dist * POINTER_MS_PER_PX))
22
+ );
23
+ }
24
+ function settleMs(step) {
25
+ if (typeof step.ms === "number" && step.ms >= 0) return step.ms;
26
+ const d = step.do;
27
+ return SETTLE_MS[d] ?? 0;
28
+ }
29
+ var easeInOutCubic = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
30
+ async function clockMotion(dur, at, clock) {
31
+ const start = clock.now();
32
+ let samples = 0;
33
+ for (; ; ) {
34
+ const elapsed = clock.now() - start;
35
+ if (elapsed >= dur) break;
36
+ await at(easeInOutCubic(Math.min(1, elapsed / dur)));
37
+ samples++;
38
+ const next = start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS;
39
+ const wait = Math.min(next, start + dur) - clock.now();
40
+ if (wait > 0) await clock.sleep(wait);
41
+ }
42
+ await at(1);
43
+ return samples + 1;
44
+ }
45
+ async function clockTyping(chars, delay, type, clock) {
46
+ const start = clock.now();
47
+ for (let i = 0; i < chars.length; i++) {
48
+ await type(chars[i], i);
49
+ const due = start + (i + 1) * delay;
50
+ const wait = due - clock.now();
51
+ if (wait > 0 && i < chars.length - 1) await clock.sleep(wait);
52
+ }
53
+ }
54
+ function askedMs(step) {
55
+ switch (step.do) {
56
+ case "wait":
57
+ return step.ms ?? 0;
58
+ case "hover":
59
+ return step.ms ?? 700;
60
+ case "drag":
61
+ return step.ms ?? 700;
62
+ case "type":
63
+ return (step.text?.length ?? 0) * (step.delayMs ?? 40);
64
+ default:
65
+ return 0;
66
+ }
67
+ }
68
+ function paceReport(steps) {
69
+ const askedMs2 = steps.reduce((a, s) => a + s.askedMs, 0);
70
+ const gestureMs = steps.reduce((a, s) => a + s.gestureMs, 0);
71
+ const wallMs = steps.reduce((a, s) => a + s.wallMs, 0);
72
+ const overheadMs = Math.max(0, wallMs - askedMs2 - gestureMs);
73
+ const slow = steps.filter((s) => s.wallMs > (s.askedMs + s.gestureMs) * 1.34 + 60).map((s) => ({
74
+ step: s.step,
75
+ do: s.do,
76
+ askedMs: s.askedMs,
77
+ wallMs: s.wallMs
78
+ }));
79
+ return {
80
+ askedMs: askedMs2,
81
+ gestureMs,
82
+ wallMs,
83
+ overheadMs,
84
+ overheadPct: wallMs > 0 ? Math.round(overheadMs / wallMs * 100) : 0,
85
+ slow
86
+ };
87
+ }
88
+ function paceLine(r) {
89
+ const s = (ms) => `${(ms / 1e3).toFixed(1)} s`;
90
+ const slow = r.slow.length ? `; slow: ${r.slow.map((x) => `#${x.step} ${x.do} ${s(x.wallMs)} for ${s(x.askedMs)} asked`).join(", ")}` : "";
91
+ return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`;
92
+ }
93
+
94
+ export {
95
+ PRESS_LEAD_MS,
96
+ PRESS_HOLD_MS,
97
+ SCROLL_SETTLE_MS,
98
+ TRAILING_HOLD_MS,
99
+ pointerTravelMs,
100
+ settleMs,
101
+ clockMotion,
102
+ clockTyping,
103
+ askedMs,
104
+ paceReport,
105
+ paceLine
106
+ };
107
+ //# sourceMappingURL=chunk-RDQYYRWN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin/pace.ts"],"sourcesContent":["/**\n * The recorder's pace — the pure parts.\n *\n * A take of a scripted flow should run as long as the script asks, plus the\n * gestures a person makes between the asks (the pointer's travel, the press,\n * the settle after it) and no more. Measured before this module (§4.5 of\n * the utility-clip retrospective), hovers ran 2× their ask, drags 4.8× and\n * a 25 s script recorded 42 s, for two reasons the numbers here answer:\n *\n * - The motion loops counted STEPS (one per 16 ms of the asked duration)\n * and awaited a `mouse.move` round trip per step, so a page that\n * re-renders per move (a slider) stretched every gesture by the round\n * trip. A gesture is now driven by the CLOCK: its position is a function\n * of the elapsed time, and it ends when the asked duration has elapsed,\n * however many samples the page allowed.\n * - The settles after a gesture were fixed sleeps (500 ms after a click,\n * 250 after every selector lookup). A settle is DATA: `ms` on the step\n * overrides a small default, and the script's own `wait` steps carry\n * the intended pauses.\n */\n\n/** Pointer travel: ms per CSS px, floor and ceiling — a person's hand. */\nexport const POINTER_MS_PER_PX = 0.7\nexport const POINTER_MIN_MS = 250\nexport const POINTER_MAX_MS = 800\n/** The sample cadence a motion loop aims for. */\nexport const MOTION_TICK_MS = 16\n\n/** The settle after a gesture when the step names none, by verb. */\nexport const SETTLE_MS = {\n click: 150,\n type: 150,\n scroll: 200,\n drag: 80,\n} as const\n\n/** The press: the pause before the button goes down, and the hold. */\nexport const PRESS_LEAD_MS = 80\nexport const PRESS_HOLD_MS = 70\n/** The pause between a selector lookup that scrolled the page and the move. */\nexport const SCROLL_SETTLE_MS = 120\n/** The hold after the last step, so the take does not cut on a press. */\nexport const TRAILING_HOLD_MS = 400\n\n/** How long the pointer takes to travel `dist` CSS px. */\nexport function pointerTravelMs(dist: number): number {\n return Math.min(\n POINTER_MAX_MS,\n Math.max(POINTER_MIN_MS, Math.round(dist * POINTER_MS_PER_PX)),\n )\n}\n\n/** The settle after a step: its own `ms` when it names one, else the verb's. */\nexport function settleMs(step: { do: string; ms?: number }): number {\n if (typeof step.ms === 'number' && step.ms >= 0) return step.ms\n const d = step.do as keyof typeof SETTLE_MS\n return SETTLE_MS[d] ?? 0\n}\n\nexport const easeInOutCubic = (u: number): number =>\n u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2\n\n/**\n * Drive a motion by the clock: `at(u)` is called with the eased progress\n * for each sample, and the loop ends when `dur` ms have elapsed, with the\n * last call at u = 1 exactly. Between samples it sleeps to the next tick\n * only when the sample came back early; a slow sample (a page busy\n * re-rendering) is followed at once, so the gesture keeps its length and\n * loses samples, never the other way round.\n */\nexport async function clockMotion(\n dur: number,\n at: (u: number) => Promise<void>,\n clock: { now: () => number; sleep: (ms: number) => Promise<void> },\n): Promise<number> {\n const start = clock.now()\n let samples = 0\n for (;;) {\n const elapsed = clock.now() - start\n if (elapsed >= dur) break\n await at(easeInOutCubic(Math.min(1, elapsed / dur)))\n samples++\n const next =\n start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS\n const wait = Math.min(next, start + dur) - clock.now()\n if (wait > 0) await clock.sleep(wait)\n }\n await at(1)\n return samples + 1\n}\n\n/**\n * Typing paced by the clock: the i-th character is due at `start + i·delay`;\n * after each keystroke the loop sleeps to the next due time only if the\n * keystroke came back early. A slow field (an editor re-rendering per key)\n * types as fast as it can and says so in the pace report.\n */\nexport async function clockTyping(\n chars: readonly string[],\n delay: number,\n type: (ch: string, index: number) => Promise<void>,\n clock: { now: () => number; sleep: (ms: number) => Promise<void> },\n): Promise<void> {\n const start = clock.now()\n for (let i = 0; i < chars.length; i++) {\n await type(chars[i], i)\n const due = start + (i + 1) * delay\n const wait = due - clock.now()\n if (wait > 0 && i < chars.length - 1) await clock.sleep(wait)\n }\n}\n\n/** One step's pace: what the script asked for it and what it took. */\nexport interface StepPace {\n step: number\n do: string\n /** The script's own ask: a wait's ms, a hover's dwell, a drag's ms, typing's chars × delay; 0 for a click or a scroll. */\n askedMs: number\n /** The gesture the recorder adds by design: pointer travel, the press, the settle. */\n gestureMs: number\n wallMs: number\n}\n\nexport interface PaceReport {\n askedMs: number\n gestureMs: number\n wallMs: number\n /** Wall time neither asked nor a gesture: what the page and the round trips cost. */\n overheadMs: number\n overheadPct: number\n /** The steps whose wall ran past their ask plus gesture by more than a third. */\n slow: { step: number; do: string; askedMs: number; wallMs: number }[]\n}\n\n/** What a script asks of a step, in ms: the part of its wall time that is the author's. */\nexport function askedMs(step: {\n do: string\n ms?: number\n text?: string\n delayMs?: number\n}): number {\n switch (step.do) {\n case 'wait':\n return step.ms ?? 0\n case 'hover':\n return step.ms ?? 700\n case 'drag':\n return step.ms ?? 700\n case 'type':\n return (step.text?.length ?? 0) * (step.delayMs ?? 40)\n default:\n return 0\n }\n}\n\nexport function paceReport(steps: readonly StepPace[]): PaceReport {\n const askedMs = steps.reduce((a, s) => a + s.askedMs, 0)\n const gestureMs = steps.reduce((a, s) => a + s.gestureMs, 0)\n const wallMs = steps.reduce((a, s) => a + s.wallMs, 0)\n const overheadMs = Math.max(0, wallMs - askedMs - gestureMs)\n const slow = steps\n .filter((s) => s.wallMs > (s.askedMs + s.gestureMs) * 1.34 + 60)\n .map((s) => ({\n step: s.step,\n do: s.do,\n askedMs: s.askedMs,\n wallMs: s.wallMs,\n }))\n return {\n askedMs,\n gestureMs,\n wallMs,\n overheadMs,\n overheadPct: wallMs > 0 ? Math.round((overheadMs / wallMs) * 100) : 0,\n slow,\n }\n}\n\n/** The pace in one line for the log. */\nexport function paceLine(r: PaceReport): string {\n const s = (ms: number) => `${(ms / 1000).toFixed(1)} s`\n const slow = r.slow.length\n ? `; slow: ${r.slow.map((x) => `#${x.step} ${x.do} ${s(x.wallMs)} for ${s(x.askedMs)} asked`).join(', ')}`\n : ''\n return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`\n}\n"],"mappings":"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","names":["askedMs"]}
@@ -6,6 +6,19 @@ import {
6
6
  noteGivenFlag,
7
7
  trackFlags
8
8
  } from "./chunk-FACYY7VV.js";
9
+ import {
10
+ PRESS_HOLD_MS,
11
+ PRESS_LEAD_MS,
12
+ SCROLL_SETTLE_MS,
13
+ TRAILING_HOLD_MS,
14
+ askedMs,
15
+ clockMotion,
16
+ clockTyping,
17
+ paceLine,
18
+ paceReport,
19
+ pointerTravelMs,
20
+ settleMs
21
+ } from "./chunk-RDQYYRWN.js";
9
22
 
10
23
  // src/plugin/run.ts
11
24
  import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
@@ -101,8 +114,137 @@ function createReporter(json) {
101
114
  };
102
115
  }
103
116
 
117
+ // src/plugin/setup.ts
118
+ var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto"]);
119
+ function validateSetup(value) {
120
+ const errors = [];
121
+ if (!Array.isArray(value)) return ["setup must be an array of steps"];
122
+ value.forEach((raw, i) => {
123
+ const at2 = `setup[${i}]`;
124
+ if (typeof raw !== "object" || raw === null) {
125
+ errors.push(`${at2}: must be an object`);
126
+ return;
127
+ }
128
+ const s = raw;
129
+ if (typeof s.do !== "string" || !VERBS.has(s.do)) {
130
+ errors.push(`${at2}: "do" must be one of ${[...VERBS].join(", ")}`);
131
+ return;
132
+ }
133
+ if ((s.do === "click" || s.do === "type") && typeof s.selector !== "string")
134
+ errors.push(`${at2}: ${s.do} needs a selector`);
135
+ if (s.do === "wait" && typeof s.ms !== "number")
136
+ errors.push(`${at2}: wait needs ms`);
137
+ if (s.do === "press" && typeof s.key !== "string")
138
+ errors.push(`${at2}: press needs a key`);
139
+ if (s.do === "goto" && typeof s.url !== "string")
140
+ errors.push(`${at2}: goto needs a url`);
141
+ if (s.do === "type") {
142
+ const t = s.text;
143
+ const isEnv = typeof t === "object" && t !== null && typeof t.env === "string" && Object.keys(t).length === 1;
144
+ if (typeof t !== "string" && !isEnv)
145
+ errors.push(
146
+ `${at2}: type needs text, a string or { "env": "NAME" } read at run time`
147
+ );
148
+ if (typeof t === "string" && /passw|secret|token/i.test(String(s.selector)) && t.length > 0)
149
+ errors.push(
150
+ `${at2}: a literal value typed into ${String(s.selector)} would live in actions.json, which is committed and pushed. Use { "env": "NAME" }`
151
+ );
152
+ }
153
+ });
154
+ return errors;
155
+ }
156
+ function resolveSetupText(t, env = process.env) {
157
+ if (typeof t === "string") return { value: t, secret: false };
158
+ const v = env[t.env];
159
+ if (v === void 0 || v === "")
160
+ throw new SetupEnvError(
161
+ `setup: the environment variable ${t.env} is not set, and a type step reads it. Export it in the shell that runs vos record; it is never written to a file`
162
+ );
163
+ return { value: v, secret: true };
164
+ }
165
+ async function runSetup(page, steps, opts) {
166
+ const lines = [];
167
+ for (const [i, step] of steps.entries()) {
168
+ const settle = "ms" in step && typeof step.ms === "number" ? step.ms : 150;
169
+ try {
170
+ switch (step.do) {
171
+ case "wait":
172
+ await opts.sleep(step.ms);
173
+ lines.push(`setup #${i} wait ${step.ms}ms`);
174
+ break;
175
+ case "goto":
176
+ await page.goto(step.url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
177
+ });
178
+ lines.push(`setup #${i} goto ${step.url}`);
179
+ break;
180
+ case "press":
181
+ await page.keyboard.press(step.key);
182
+ await opts.sleep(settle);
183
+ lines.push(`setup #${i} press ${step.key}`);
184
+ break;
185
+ case "click": {
186
+ const loc = page.locator(step.selector).first();
187
+ await loc.waitFor({ state: "visible", timeout: 8e3 });
188
+ await loc.click();
189
+ await opts.sleep(settle);
190
+ lines.push(`setup #${i} click ${step.selector}`);
191
+ break;
192
+ }
193
+ case "type": {
194
+ const loc = page.locator(step.selector).first();
195
+ await loc.waitFor({ state: "visible", timeout: 8e3 });
196
+ const { value, secret } = resolveSetupText(step.text, opts.env);
197
+ await loc.fill(value);
198
+ await opts.sleep(settle);
199
+ lines.push(
200
+ `setup #${i} type ${secret ? `\${${step.text.env}}` : `${value.length} chars`} into ${step.selector}`
201
+ );
202
+ break;
203
+ }
204
+ }
205
+ } catch (e) {
206
+ if (e instanceof SetupEnvError) throw e;
207
+ return {
208
+ ran: i,
209
+ lines,
210
+ failed: {
211
+ step: i,
212
+ do: step.do,
213
+ ..."selector" in step ? { selector: step.selector } : {}
214
+ }
215
+ };
216
+ }
217
+ }
218
+ return { ran: steps.length, lines };
219
+ }
220
+ var SetupEnvError = class extends Error {
221
+ constructor(message) {
222
+ super(message);
223
+ this.name = "SetupEnvError";
224
+ }
225
+ };
226
+ var SetupError = class extends Error {
227
+ constructor(failed2) {
228
+ super(
229
+ `setup #${failed2.step} ${failed2.do}${failed2.selector ? ` ${failed2.selector}` : ""}: the selector never appeared, so the take would begin at a half-finished setup. Nothing was recorded. Fix the setup step (rehearse with --dry-run), then record.`
230
+ );
231
+ this.failed = failed2;
232
+ this.name = "SetupError";
233
+ }
234
+ failed;
235
+ };
236
+ function parseHeaders(given) {
237
+ const out = {};
238
+ for (const h of given ?? []) {
239
+ const i = h.indexOf("=");
240
+ if (i <= 0) throw new Error(`--header wants name=value, got "${h}"`);
241
+ out[h.slice(0, i).trim()] = h.slice(i + 1);
242
+ }
243
+ return out;
244
+ }
245
+
104
246
  // src/plugin/actions.ts
105
- var VERBS = /* @__PURE__ */ new Set([
247
+ var VERBS2 = /* @__PURE__ */ new Set([
106
248
  "wait",
107
249
  "hover",
108
250
  "click",
@@ -129,6 +271,7 @@ function validateActions(value) {
129
271
  }
130
272
  }
131
273
  }
274
+ if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
132
275
  if (obj.mask !== void 0) {
133
276
  if (!Array.isArray(obj.mask)) {
134
277
  errors.push("mask must be an array of { selector, as?, text? }");
@@ -159,8 +302,8 @@ function validateActions(value) {
159
302
  return;
160
303
  }
161
304
  const s = raw;
162
- if (typeof s.do !== "string" || !VERBS.has(s.do)) {
163
- errors.push(`${at2}: "do" must be one of ${[...VERBS].join(", ")}`);
305
+ if (typeof s.do !== "string" || !VERBS2.has(s.do)) {
306
+ errors.push(`${at2}: "do" must be one of ${[...VERBS2].join(", ")}`);
164
307
  return;
165
308
  }
166
309
  if (s.id !== void 0) {
@@ -6556,97 +6699,6 @@ function cappedLine(capSeconds) {
6556
6699
  return `stopped at ${formatDurationCap(capSeconds)} (--max-duration ${capSeconds}); the remaining steps did not run`;
6557
6700
  }
6558
6701
 
6559
- // src/plugin/pace.ts
6560
- var POINTER_MS_PER_PX = 0.7;
6561
- var POINTER_MIN_MS = 250;
6562
- var POINTER_MAX_MS = 800;
6563
- var MOTION_TICK_MS = 16;
6564
- var SETTLE_MS = {
6565
- click: 150,
6566
- type: 150,
6567
- scroll: 200,
6568
- drag: 80
6569
- };
6570
- var PRESS_LEAD_MS = 80;
6571
- var PRESS_HOLD_MS = 70;
6572
- var SCROLL_SETTLE_MS = 120;
6573
- var TRAILING_HOLD_MS = 400;
6574
- function pointerTravelMs(dist) {
6575
- return Math.min(
6576
- POINTER_MAX_MS,
6577
- Math.max(POINTER_MIN_MS, Math.round(dist * POINTER_MS_PER_PX))
6578
- );
6579
- }
6580
- function settleMs(step) {
6581
- if (typeof step.ms === "number" && step.ms >= 0) return step.ms;
6582
- const d = step.do;
6583
- return SETTLE_MS[d] ?? 0;
6584
- }
6585
- var easeInOutCubic = (u) => u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
6586
- async function clockMotion(dur, at2, clock) {
6587
- const start = clock.now();
6588
- let samples = 0;
6589
- for (; ; ) {
6590
- const elapsed = clock.now() - start;
6591
- if (elapsed >= dur) break;
6592
- await at2(easeInOutCubic(Math.min(1, elapsed / dur)));
6593
- samples++;
6594
- const next = start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS;
6595
- const wait2 = Math.min(next, start + dur) - clock.now();
6596
- if (wait2 > 0) await clock.sleep(wait2);
6597
- }
6598
- await at2(1);
6599
- return samples + 1;
6600
- }
6601
- async function clockTyping(chars, delay, type, clock) {
6602
- const start = clock.now();
6603
- for (let i = 0; i < chars.length; i++) {
6604
- await type(chars[i], i);
6605
- const due = start + (i + 1) * delay;
6606
- const wait2 = due - clock.now();
6607
- if (wait2 > 0 && i < chars.length - 1) await clock.sleep(wait2);
6608
- }
6609
- }
6610
- function askedMs(step) {
6611
- switch (step.do) {
6612
- case "wait":
6613
- return step.ms ?? 0;
6614
- case "hover":
6615
- return step.ms ?? 700;
6616
- case "drag":
6617
- return step.ms ?? 700;
6618
- case "type":
6619
- return (step.text?.length ?? 0) * (step.delayMs ?? 40);
6620
- default:
6621
- return 0;
6622
- }
6623
- }
6624
- function paceReport(steps) {
6625
- const askedMs2 = steps.reduce((a, s) => a + s.askedMs, 0);
6626
- const gestureMs = steps.reduce((a, s) => a + s.gestureMs, 0);
6627
- const wallMs = steps.reduce((a, s) => a + s.wallMs, 0);
6628
- const overheadMs = Math.max(0, wallMs - askedMs2 - gestureMs);
6629
- const slow = steps.filter((s) => s.wallMs > (s.askedMs + s.gestureMs) * 1.34 + 60).map((s) => ({
6630
- step: s.step,
6631
- do: s.do,
6632
- askedMs: s.askedMs,
6633
- wallMs: s.wallMs
6634
- }));
6635
- return {
6636
- askedMs: askedMs2,
6637
- gestureMs,
6638
- wallMs,
6639
- overheadMs,
6640
- overheadPct: wallMs > 0 ? Math.round(overheadMs / wallMs * 100) : 0,
6641
- slow
6642
- };
6643
- }
6644
- function paceLine(r) {
6645
- const s = (ms) => `${(ms / 1e3).toFixed(1)} s`;
6646
- const slow = r.slow.length ? `; slow: ${r.slow.map((x) => `#${x.step} ${x.do} ${s(x.wallMs)} for ${s(x.askedMs)} asked`).join(", ")}` : "";
6647
- return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`;
6648
- }
6649
-
6650
6702
  // src/plugin/wall.ts
6651
6703
  var IDP_HOSTS = [
6652
6704
  "accounts.google.com",
@@ -6994,7 +7046,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6994
7046
  const context = await browser.newContext({
6995
7047
  viewport: { width: vw, height: vh },
6996
7048
  deviceScaleFactor: 1,
6997
- ...opts.storageState ? { storageState: opts.storageState } : {}
7049
+ ...opts.storageState ? { storageState: opts.storageState } : {},
7050
+ ...opts.headers && Object.keys(opts.headers).length ? { extraHTTPHeaders: opts.headers } : {}
6998
7051
  });
6999
7052
  const masks = actions.mask ?? [];
7000
7053
  if (masks.length) await context.addInitScript(maskInitScript(masks));
@@ -7009,12 +7062,26 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
7009
7062
  });
7010
7063
  log(`goto ${url}`);
7011
7064
  let navTimeout = false;
7012
- const response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
7065
+ let response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
7013
7066
  navTimeout = true;
7014
7067
  log(" (networkidle timeout \u2014 continuing)");
7015
7068
  return null;
7016
7069
  });
7017
7070
  await sleep(800);
7071
+ const setup = actions.setup ?? [];
7072
+ if (setup.length) {
7073
+ log(`setup: ${setup.length} step(s), off camera`);
7074
+ const result = await runSetup(page, setup, { sleep: realSleep });
7075
+ for (const line of result.lines) log(` ${line}`);
7076
+ if (result.failed) {
7077
+ await context.close().catch(() => {
7078
+ });
7079
+ throw new SetupError(result.failed);
7080
+ }
7081
+ log(`goto ${url} (after setup)`);
7082
+ response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => null);
7083
+ await sleep(800);
7084
+ }
7018
7085
  let wall = null;
7019
7086
  if (opts.onArrival) {
7020
7087
  const seen = await page.evaluate(WALL_PROBE).catch(() => null);
@@ -10443,9 +10510,10 @@ function convertAgentBrowser(records, opts = {}) {
10443
10510
  async function cmdActions(argv) {
10444
10511
  const { positionals, flags } = parseArgs(argv, /* @__PURE__ */ new Set(["json"]));
10445
10512
  const [sub, input] = positionals;
10513
+ if (sub === "script" && input) return cmdActionsScript(input, flags);
10446
10514
  if (sub !== "from-agent-browser" || !input) {
10447
10515
  throw new UsageError(
10448
- "vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]"
10516
+ "vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]\n vos actions script <actions.json> [--json]"
10449
10517
  );
10450
10518
  }
10451
10519
  const r = createReporter(flags.json === true);
@@ -10493,6 +10561,18 @@ function summary(result) {
10493
10561
  notes: result.notes
10494
10562
  };
10495
10563
  }
10564
+ async function cmdActionsScript(input, flags) {
10565
+ const r = createReporter(flags.json === true);
10566
+ const { readFile: readFile16 } = await import("fs/promises");
10567
+ const { shotList, formatShotList } = await import("./shotList-FYDHO5AI.js");
10568
+ const raw = JSON.parse(await readFile16(resolve8(input), "utf8"));
10569
+ const errors = validateActions(raw);
10570
+ if (errors.length) throw new UsageError(`${input}:
10571
+ ${errors.join("\n ")}`);
10572
+ const list = shotList(raw);
10573
+ r.done({ ...list, text: formatShotList(list) }, formatShotList(list));
10574
+ return EXIT_OK;
10575
+ }
10496
10576
 
10497
10577
  // src/plugin/run.ts
10498
10578
  var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
@@ -10518,12 +10598,12 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
10518
10598
  "dry-run",
10519
10599
  "allow-wall"
10520
10600
  ]);
10521
- var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg"]);
10601
+ var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
10522
10602
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
10523
10603
 
10524
10604
  Take pipeline
10525
- vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
10526
- vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
10605
+ vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
10606
+ vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
10527
10607
  vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
10528
10608
  vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
10529
10609
  vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
@@ -10535,6 +10615,8 @@ Take pipeline
10535
10615
  vos validate <actions.json|take|kit.json> [--picture] [--json]
10536
10616
  vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
10537
10617
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
10618
+ vos actions script <actions.json> [--json]
10619
+ the flow as numbered beats in words, for a person recording it by hand
10538
10620
 
10539
10621
  Platform (vos.so) \u2014 fetch, edit, push, pull, repeat
10540
10622
  vos login [--key <vos_sk_\u2026>] [--label <name>] [--no-browser]
@@ -10816,6 +10898,13 @@ function exposureNote(rec) {
10816
10898
  ${rec.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
10817
10899
  ${EXPOSURE_ADVICE}`;
10818
10900
  }
10901
+ function takeHeaders(multi) {
10902
+ try {
10903
+ return parseHeaders(multi.header);
10904
+ } catch (e) {
10905
+ throw new UsageError(e instanceof Error ? e.message : String(e));
10906
+ }
10907
+ }
10819
10908
  function takeBrowserArgs(multi) {
10820
10909
  const given = multi["browser-arg"];
10821
10910
  return (given ?? []).filter(Boolean);
@@ -10886,6 +10975,7 @@ async function cmdRecord(argv) {
10886
10975
  const maxDurationSeconds = await maxDuration(flags, r);
10887
10976
  const storageState = takeStorageState(flags);
10888
10977
  const browserArgs = takeBrowserArgs(multi);
10978
+ const headers = takeHeaders(multi);
10889
10979
  if (flags["dry-run"] === true) {
10890
10980
  void flags.strict;
10891
10981
  void flags["keep-frames"];
@@ -10902,6 +10992,7 @@ async function cmdRecord(argv) {
10902
10992
  r.log,
10903
10993
  {
10904
10994
  storageState,
10995
+ headers,
10905
10996
  dryRun: true,
10906
10997
  // A rehearsal is always strict, and it touches no directory.
10907
10998
  onArrival: wallGate(flags, r, { strict: true })
@@ -10917,6 +11008,7 @@ async function cmdRecord(argv) {
10917
11008
  const carried = [
10918
11009
  ...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
10919
11010
  ...browserArgs.map((a) => `--browser-arg=${a}`),
11011
+ ...Object.entries(headers).map(([k, v]) => `--header ${k}=${v}`),
10920
11012
  ...flags["allow-wall"] === true ? ["--allow-wall"] : []
10921
11013
  ].join(" ");
10922
11014
  const nextOut = strFlag(flags, "out") ?? "take";
@@ -10958,6 +11050,7 @@ ${lines.join("\n")}
10958
11050
  const rec = await recordTake(browser, url, actions, paths, r.log, {
10959
11051
  maxDurationSeconds,
10960
11052
  storageState,
11053
+ headers,
10961
11054
  onArrival: wallGate(flags, r, {
10962
11055
  strict: flags.strict === true,
10963
11056
  prepare
@@ -11038,6 +11131,7 @@ async function cmdCreate2(argv) {
11038
11131
  const maxDurationSeconds = await maxDuration(flags, r);
11039
11132
  const storageState = takeStorageState(flags);
11040
11133
  const browserArgs = takeBrowserArgs(multi);
11134
+ const headers = takeHeaders(multi);
11041
11135
  const paths = takePaths(outDir);
11042
11136
  const prepare = async () => {
11043
11137
  if (existsSync15(join18(outDir, "meta.json"))) {
@@ -11056,6 +11150,7 @@ async function cmdCreate2(argv) {
11056
11150
  const rec = await recordTake(browser, url, actions, paths, r.log, {
11057
11151
  maxDurationSeconds,
11058
11152
  storageState,
11153
+ headers,
11059
11154
  onArrival: wallGate(flags, r, {
11060
11155
  strict: flags.strict === true,
11061
11156
  prepare
@@ -12196,6 +12291,11 @@ async function run(argv) {
12196
12291
  } catch (e) {
12197
12292
  if (e instanceof UsageError) {
12198
12293
  process.stderr.write(`usage error: ${e.message}
12294
+ `);
12295
+ return EXIT_USAGE;
12296
+ }
12297
+ if (e instanceof SetupError || e instanceof SetupEnvError) {
12298
+ process.stderr.write(`${e.message}
12199
12299
  `);
12200
12300
  return EXIT_USAGE;
12201
12301
  }
@@ -12248,4 +12348,4 @@ export {
12248
12348
  verbHelp,
12249
12349
  run
12250
12350
  };
12251
- //# sourceMappingURL=chunk-7RMULTDM.js.map
12351
+ //# sourceMappingURL=chunk-WCSAECM4.js.map