@vosjs/cli 0.80.0 → 0.81.1

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/dist/cli.js CHANGED
@@ -839,7 +839,7 @@ async function cmdCheck(argv) {
839
839
  const result = runCheck(parsed);
840
840
  if (result.config) {
841
841
  const baseDir = baseDirOf(source);
842
- const { hostedLiteralWarnings } = await import("./assetManifest-ETT3C7ZA.js");
842
+ const { hostedLiteralWarnings } = await import("./assetManifest-27NKSS7N.js");
843
843
  const extra = [
844
844
  ...baseDir ? missingManifestFiles(result.config, baseDir) : [],
845
845
  ...hostedLiteralWarnings(result.config)
@@ -873,7 +873,7 @@ async function cmdCheck(argv) {
873
873
  return result.ok ? EXIT_OK : EXIT_ERROR;
874
874
  }
875
875
  async function delegate(argv) {
876
- const { run } = await import("./run-2ELHMPTB.js");
876
+ const { run } = await import("./run-MEDD2Q74.js");
877
877
  return await run(argv);
878
878
  }
879
879
  async function printHelp() {
@@ -907,7 +907,7 @@ async function main() {
907
907
  const engine = HELP_ENGINE.split("\n").filter(
908
908
  (l) => l.startsWith(` vos ${cmd} `)
909
909
  );
910
- const { verbHelp } = await import("./run-2ELHMPTB.js");
910
+ const { verbHelp } = await import("./run-MEDD2Q74.js");
911
911
  const take = verbHelp(cmd);
912
912
  process.stdout.write(
913
913
  `${engine.join("\n")}
@@ -935,7 +935,7 @@ ${take.includes("no such verb") ? "" : take}`
935
935
  main().then(async (code) => {
936
936
  if (code === EXIT_OK) {
937
937
  const verb = process.argv[2] ?? "";
938
- const { HELP } = await import("./run-2ELHMPTB.js");
938
+ const { HELP } = await import("./run-MEDD2Q74.js");
939
939
  const documented = [
940
940
  ...helpFlagsFor(HELP_ENGINE, verb),
941
941
  ...helpFlagsFor(HELP, verb)
package/dist/index.d.ts CHANGED
@@ -137,33 +137,6 @@ type RecordOpts = Omit<RecordOpts$1, 'context' | 'env' | 'platform' | 'outputs'
137
137
  };
138
138
  declare function recordTake(browser: Browser, url: string, actions: ActionsFile, paths: TakePaths, log: (msg: string) => void, opts?: RecordOpts): Promise<_vosjs_render_core_record.RecordResult>;
139
139
 
140
- /** Entry point — the delegation contract for @vosjs/cli: the host forwards
141
- every non-engine verb here (`vos <verb>`). The exported `manifest` (index.ts)
142
- tells the host which verbs exist and how to summarize them in help. */
143
- declare function run(argv: string[]): Promise<number>;
144
-
145
- /**
146
- * The verb manifest of the take pipeline and the vos.so verbs — what
147
- * `vos help` lists under the engine verbs. It kept the shape a separately
148
- * installed plugin once handed the host (name + host range), so a script
149
- * reading it keeps working.
150
- */
151
- interface PluginVerb {
152
- name: string;
153
- summary: string;
154
- }
155
- interface PluginManifest {
156
- name: string;
157
- /** Host versions this plugin speaks the run(argv) contract with. */
158
- hostRange: string;
159
- verbs: PluginVerb[];
160
- }
161
- declare const manifest: PluginManifest;
162
-
163
- declare function encodeRecording(browser: Browser, takeDir: string, onProgress: (fraction: number) => void): Promise<{
164
- bytes: number;
165
- }>;
166
-
167
140
  /** The release's words a plan writes into the cut's titles and end card. */
168
141
  interface ReleaseWords {
169
142
  headline?: string | null;
@@ -316,6 +289,33 @@ interface PlanOptions {
316
289
  }
317
290
  declare function planTake(dir: string, opts?: PlanOptions): Promise<PlanSummary>;
318
291
 
292
+ /** Entry point — the delegation contract for @vosjs/cli: the host forwards
293
+ every non-engine verb here (`vos <verb>`). The exported `manifest` (index.ts)
294
+ tells the host which verbs exist and how to summarize them in help. */
295
+ declare function run(argv: string[]): Promise<number>;
296
+
297
+ /**
298
+ * The verb manifest of the take pipeline and the vos.so verbs — what
299
+ * `vos help` lists under the engine verbs. It kept the shape a separately
300
+ * installed plugin once handed the host (name + host range), so a script
301
+ * reading it keeps working.
302
+ */
303
+ interface PluginVerb {
304
+ name: string;
305
+ summary: string;
306
+ }
307
+ interface PluginManifest {
308
+ name: string;
309
+ /** Host versions this plugin speaks the run(argv) contract with. */
310
+ hostRange: string;
311
+ verbs: PluginVerb[];
312
+ }
313
+ declare const manifest: PluginManifest;
314
+
315
+ declare function encodeRecording(browser: Browser, takeDir: string, onProgress: (fraction: number) => void): Promise<{
316
+ bytes: number;
317
+ }>;
318
+
319
319
  interface DigestOptions {
320
320
  outDir?: string;
321
321
  full?: number;
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  startTakeServer,
15
15
  validateActions,
16
16
  waitForPageDone
17
- } from "./chunk-SP5OHTVB.js";
17
+ } from "./chunk-S3RI2SHH.js";
18
18
  import {
19
19
  BrowserUnavailableError,
20
20
  configDuration,
@@ -29,7 +29,7 @@ import {
29
29
  loadTake,
30
30
  pullMedia,
31
31
  takePaths
32
- } from "./chunk-PJ6MEJZV.js";
32
+ } from "./chunk-352AC3V6.js";
33
33
  import {
34
34
  apiError,
35
35
  apiJson,
@@ -2,23 +2,27 @@
2
2
  import {
3
3
  HELP,
4
4
  MULTI_FLAGS,
5
+ previousCutReport,
6
+ reRecord,
5
7
  run,
6
8
  strictFails,
7
9
  takeBackdrop,
8
10
  takeBrowserArgs,
9
11
  verbHelp
10
- } from "./chunk-SP5OHTVB.js";
12
+ } from "./chunk-S3RI2SHH.js";
11
13
  import "./chunk-B373NVJE.js";
12
- import "./chunk-PJ6MEJZV.js";
14
+ import "./chunk-352AC3V6.js";
13
15
  import "./chunk-J3KHIZCH.js";
14
16
  import "./chunk-6XPFWQKP.js";
15
17
  export {
16
18
  HELP,
17
19
  MULTI_FLAGS,
20
+ previousCutReport,
21
+ reRecord,
18
22
  run,
19
23
  strictFails,
20
24
  takeBackdrop,
21
25
  takeBrowserArgs,
22
26
  verbHelp
23
27
  };
24
- //# sourceMappingURL=run-2ELHMPTB.js.map
28
+ //# sourceMappingURL=run-MEDD2Q74.js.map
@@ -143,6 +143,7 @@ function shotList(actions) {
143
143
  // gestures the recorder adds around each ask: about a second a beat
144
144
  aboutS: Math.round((total + beats.length * 1e3) / 1e3),
145
145
  setup: actions.setup?.length ?? 0,
146
+ ...actions.teardown?.length ? { teardown: actions.teardown.length } : {},
146
147
  ...peers.length ? { peers } : {}
147
148
  };
148
149
  }
@@ -173,6 +174,10 @@ function formatShotList(list) {
173
174
  out.push(
174
175
  "Move at a talking pace, let each screen settle before the next beat, and end on the finished state. Press the icon again to stop; the take opens in the studio and saves to your shelf."
175
176
  );
177
+ if (list.teardown)
178
+ out.push(
179
+ `After you stop: the script's ${list.teardown} teardown step${list.teardown === 1 ? "" : "s"} (remove what the take created) are yours to do; they are not part of the take.`
180
+ );
176
181
  return out.join("\n");
177
182
  }
178
183
  export {
@@ -182,4 +187,4 @@ export {
182
187
  sayTarget,
183
188
  shotList
184
189
  };
185
- //# sourceMappingURL=shotList-MSNWLVDN.js.map
190
+ //# sourceMappingURL=shotList-PL7XP6AC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin/shotList.ts"],"sourcesContent":["/**\n * The shot list: an `actions.json` said in words, for a person who will\n * record it by hand. The last rung of the session ladder is \"the human\n * records, the agent cuts\", and an agent that falls to it should hand over\n * more than an apology: the flow it worked out, as numbered beats with the\n * pace it wanted, so its knowledge survives the hop to a human. The take\n * comes back through the ordinary handoff and the agent cuts it.\n *\n * Pure: an ActionsFile in, lines out. Selectors are said as what they name\n * where the script says (an id, a caption, a text= locator), and as the\n * selector itself where it does not: a person reading `#filter-late` can\n * find it; a person reading nothing cannot.\n */\nimport type { ActionStep, ActionsFile } from './actions'\nimport { askedMs } from './pace'\nimport { pressSequence, splitChord } from '@vosjs/render-core/record'\n\n/** A key or a chord as a person reads it: `Mod` is ⌘ on a Mac, Ctrl elsewhere. */\nexport function sayChord(chord: string): string {\n const { mods, key } = splitChord(chord)\n const word = (k: string) =>\n k === 'Mod' || k === 'ControlOrMeta'\n ? '⌘/Ctrl'\n : k === 'Control'\n ? 'Ctrl'\n : k === 'Meta'\n ? '⌘'\n : k === 'Alt'\n ? 'Alt/Option'\n : k === ' '\n ? 'Space'\n : k\n return [...mods, key].map(word).join('+')\n}\n\n/** A viewport point as a person finds it: where it is, and the script's numbers. */\nfunction sayPoint(p: { x?: number; y?: number }): string {\n const x = p.x ?? 0\n const y = p.y ?? 0\n return `the spot at ${Math.round(x)}, ${Math.round(y)} (${y < 360 ? 'upper' : 'lower'} ${x < 640 ? 'left' : 'right'})`\n}\n\n/** A list in words: `a`, `a then b`, `a, b then c`. */\nconst thenList = (xs: string[]): string =>\n xs.length <= 1\n ? (xs[0] ?? '')\n : `${xs.slice(0, -1).join(', ')} then ${xs[xs.length - 1]}`\n\nexport interface ShotBeat {\n n: number\n /** what to do, in words */\n line: string\n /** how long to hold after, in seconds, when the script asks for a hold */\n holdS?: number\n}\n\nexport interface ShotList {\n url?: string\n viewport?: { width: number; height: number }\n beats: ShotBeat[]\n /** the take's asked length, from the script's own pacing */\n aboutS: number\n setup: number\n /** The script's teardown steps: what the person undoes once they stop. */\n teardown?: number\n /**\n * What each other person does (the script's `peers`), as beats tied to\n * the take's own: a second pair of hands, in their own browser, on the\n * same page.\n */\n peers?: { id: string; lines: string[] }[]\n}\n\n/** A selector as a person reads it: the text it names, or the selector itself. */\nexport function sayTarget(selector: string): string {\n const text = /^text=(['\"]?)(.+?)\\1$/.exec(selector)\n if (text) return `\"${text[2]}\"`\n const has = /:has-text\\((['\"])(.+?)\\1\\)/.exec(selector)\n if (has) return `\"${has[2]}\"`\n const role = /getByRole\\(['\"](\\w+)['\"],\\s*\\{\\s*name:\\s*(['\"])(.+?)\\2/.exec(\n selector,\n )\n if (role) return `the ${role[1]} \"${role[3]}\"`\n const label = /\\[aria-label=(['\"])(.+?)\\1\\]/.exec(selector)\n if (label) return `\"${label[2]}\"`\n const ph = /\\[placeholder=(['\"])(.+?)\\1\\]/.exec(selector)\n if (ph) return `the \"${ph[2]}\" field`\n return `\\`${selector}\\``\n}\n\nconst s1 = (ms: number) => `${Math.round(ms / 100) / 10}`.replace(/\\.0$/, '')\n\n/** A step's id, said as a name when it reads as one (`late`, `new-order`, `openBilling`). */\nexport function sayId(id: string | undefined): string | null {\n if (!id || /^\\d+$/.test(id) || id.length < 3) return null\n const words = id\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .replace(/[-_]+/g, ' ')\n .toLowerCase()\n .trim()\n return words.length >= 3 ? words : null\n}\n\n/** What a step points at, for a person: the words the selector names, else the id's, else the selector. */\nfunction sayStepTarget(step: { selector: string; id?: string }): string {\n const bySelector = sayTarget(step.selector)\n if (!bySelector.startsWith('`')) return bySelector\n const byId = sayId(step.id)\n return byId ? `${byId} (${bySelector})` : bySelector\n}\n\n/** A page condition, for a person: what they should see before going on. */\nfunction sayCondition(c: {\n selector?: string\n text?: string\n gone?: string\n enabled?: boolean\n js?: string\n equals?: unknown\n id?: string\n}): string {\n // A state the app keeps in script: a person reads it in the browser's\n // console, so the beat names the expression to run there.\n if (c.js !== undefined)\n return c.equals !== undefined\n ? `the console's \\`${c.js}\\` gives ${JSON.stringify(c.equals)}`\n : `the console's \\`${c.js}\\` is true`\n if (c.text !== undefined) return `\"${c.text}\" shows on the page`\n if (c.gone !== undefined) return `${sayTarget(c.gone)} is gone`\n const target = sayStepTarget({ selector: c.selector ?? '', id: c.id })\n if (c.enabled === true) return `${target} is enabled`\n if (c.enabled === false) return `${target} is disabled`\n return `${target} is showing`\n}\n\nfunction beatOf(step: ActionStep): string | null {\n const line = gestureBeat(step)\n if (!line) return line\n // What the cut's camera does here, said where a person reads it: a step\n // marked as the beat's payoff is zoomed in on, one marked against it is not.\n const camera =\n step.zoom === true\n ? ' (zoomed in on in the cut)'\n : step.zoom === false\n ? ' (the cut stays wide here)'\n : ''\n // A step that spends something real is marked where a person reads it.\n return step.guard\n ? `${line}${camera} (this spends something real: do it only if you mean to)`\n : `${line}${camera}`\n}\n\nfunction gestureBeat(step: ActionStep): string | null {\n const cap = step.caption\n const named = cap ? `${cap}: ` : ''\n switch (step.do) {\n case 'expect':\n return `${named}Check that ${sayCondition(step)}`\n case 'await':\n return `${named}Wait until ${sayCondition({ ...step.until, id: step.id })} (off camera: pause the recording meanwhile)`\n case 'download':\n return step.selector !== undefined\n ? `${named}Download the file ${sayStepTarget({ selector: step.selector, id: step.id })} gives${step.as ? `, as ${step.as}` : ''}`\n : `${named}Download what ${sayTarget(step.src ?? '')} plays${step.as ? `, as ${step.as}` : ''}`\n case 'wait':\n return null\n case 'hover':\n return `${named}Rest the pointer on ${step.selector !== undefined ? sayStepTarget({ selector: step.selector, id: step.id }) : sayPoint(step)}`\n case 'click': {\n const verb =\n step.count === 3\n ? 'Triple-click'\n : step.count === 2\n ? 'Double-click'\n : 'Click'\n const button =\n step.button === 'right'\n ? ' with the right button'\n : step.button === 'middle'\n ? ' with the middle button'\n : ''\n const target =\n step.selector !== undefined\n ? sayStepTarget({ selector: step.selector, id: step.id })\n : sayPoint(step)\n const holding = step.modifiers?.length\n ? `Hold ${step.modifiers.map(sayChord).join('+')} and ${verb.toLowerCase()}`\n : verb\n return `${named}${holding} ${target}${button}`\n }\n case 'type':\n return `${named}${step.replace ? 'Replace what is in' : 'Type'}${step.replace ? ` ${sayStepTarget(step)} with \"${step.text}\"` : ` \"${step.text}\" into ${sayStepTarget(step)}`}${step.at === 'end' ? ', at the end' : step.at === 'start' ? ', at the start' : ''}`\n case 'press':\n return `${named}Press ${thenList(pressSequence(step).map(sayChord))}${step.selector ? ` in ${sayStepTarget(step as { selector: string; id?: string })}` : ''}`\n case 'paste': {\n const what =\n step.text !== undefined\n ? `\"${step.text.length > 60 ? `${step.text.slice(0, 57)}...` : step.text}\"`\n : `the file ${step.file}`\n return `${named}Copy ${what}, then paste it${step.selector ? ` into ${sayStepTarget({ selector: step.selector, id: step.id })}` : ''} (⌘/Ctrl+V)`\n }\n case 'scroll':\n return `${named}Scroll ${step.dy < 0 ? 'up' : 'down'} ${step.dy < 0 ? 'a little' : 'a little'}`\n case 'move':\n return `${named}Move the pointer to the ${step.y < 360 ? 'upper' : 'lower'} ${step.x < 640 ? 'left' : 'right'}`\n case 'drag': {\n const holding = step.modifiers?.length\n ? `Hold ${step.modifiers.map(sayChord).join('+')} and `\n : ''\n const verb = holding ? 'drag' : 'Drag'\n if (step.path !== undefined)\n return `${named}${holding}${holding ? 'draw' : 'Draw'} the stroke ${typeof step.path === 'string' ? `in ${step.path}` : `of ${step.path.length} points`}, in one press`\n return `${named}${holding}${verb} ${step.selector ? sayStepTarget(step as { selector: string; id?: string }) : 'from the start point'} across`\n }\n }\n}\n\nexport function shotList(actions: ActionsFile): ShotList {\n const beats: ShotBeat[] = []\n let pendingHold = 0\n let total = 0\n // Which beat a take step is (or, for a wait, the beat it holds), so a\n // peer's step can say when it happens.\n const beatOfId = new Map<string, number>()\n for (const step of actions.steps) {\n if (step.id)\n beatOfId.set(\n step.id,\n Math.max(1, beats.length + (step.do === 'wait' ? 0 : 1)),\n )\n total += askedMs(step)\n if (step.do === 'wait') {\n // A wait after a beat is that beat's hold; a leading wait is the\n // settle before the first, which a person does without being told.\n if (beats.length) {\n const last = beats[beats.length - 1]\n last.holdS = (last.holdS ?? 0) + step.ms / 1000\n } else pendingHold += step.ms\n continue\n }\n const line = beatOf(step)\n if (!line) continue\n const beat: ShotBeat = { n: beats.length + 1, line }\n if (step.do === 'hover') beat.holdS = askedMs(step) / 1000\n beats.push(beat)\n }\n void pendingHold\n const peers = (actions.peers ?? []).map((peer) => ({\n id: peer.id,\n lines: peer.steps\n .map((step) => {\n const line = step.do === 'wait' ? `Wait ${s1(step.ms)} s` : beatOf(step)\n if (!line) return null\n const when = step.with\n ? `As beat ${beatOfId.get(step.with) ?? '?'} starts: `\n : step.after\n ? `Once beat ${beatOfId.get(step.after) ?? '?'} is done: `\n : ''\n return `${when}${line}`\n })\n .filter((l): l is string => l !== null),\n }))\n return {\n url: actions.url,\n viewport: actions.viewport,\n beats,\n // gestures the recorder adds around each ask: about a second a beat\n aboutS: Math.round((total + beats.length * 1000) / 1000),\n setup: actions.setup?.length ?? 0,\n ...(actions.teardown?.length ? { teardown: actions.teardown.length } : {}),\n ...(peers.length ? { peers } : {}),\n }\n}\n\n/** The list as the person reads it. */\nexport function formatShotList(list: ShotList): string {\n const out: string[] = []\n out.push(\n `Record this with the vosso extension in your own signed-in browser, about ${list.aboutS} seconds${list.viewport ? `, window about ${list.viewport.width}×${list.viewport.height}` : ''}.`,\n )\n if (list.url)\n out.push(`Start on ${list.url}, signed in, with the page settled.`)\n if (list.setup)\n out.push(\n `Before you press record: the script's ${list.setup} setup step${list.setup === 1 ? '' : 's'} (sign in, dismiss banners) are yours to do first; they are not part of the take.`,\n )\n out.push('')\n for (const b of list.beats) {\n const hold =\n b.holdS && b.holdS >= 0.4 ? ` Hold ${s1(b.holdS * 1000)} s.` : ''\n out.push(`${b.n}. ${b.line}.${hold}`)\n }\n for (const peer of list.peers ?? []) {\n out.push('')\n out.push(\n `A second person (${peer.id}) does this at the same time, in their own browser on the same page; their screen is not recorded, the app shows what they do:`,\n )\n peer.lines.forEach((l, i) => out.push(` ${peer.id}${i + 1}. ${l}.`))\n }\n out.push('')\n out.push(\n 'Move at a talking pace, let each screen settle before the next beat, and end on the finished state. Press the icon again to stop; the take opens in the studio and saves to your shelf.',\n )\n if (list.teardown)\n out.push(\n `After you stop: the script's ${list.teardown} teardown step${list.teardown === 1 ? '' : 's'} (remove what the take created) are yours to do; they are not part of the take.`,\n )\n return out.join('\\n')\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vosjs/cli",
3
- "version": "0.80.0",
3
+ "version": "0.81.1",
4
4
  "description": "The vos CLI: record the real product from a scripted browser flow, auto-zoom from the cursor track, cut as data in doc.json, render deterministic video, deliver a release's video cuts per destination spec, and sync with vos.so. One binary, every verb, MIT.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,11 +52,11 @@
52
52
  "mediabunny": "^1.55.7",
53
53
  "playwright": "^1.49.0",
54
54
  "@vosjs/core": "^0.31.0",
55
- "@vosjs/editor": "^1.6.0",
56
55
  "@vosjs/elements": "^0.12.1",
57
- "@vosjs/render-core": "^0.9.0",
56
+ "@vosjs/editor": "^1.6.0",
58
57
  "@vosjs/shared": "^0.7.0",
59
- "@vosjs/studio-core": "^0.45.0",
58
+ "@vosjs/render-core": "^0.10.0",
59
+ "@vosjs/studio-core": "^0.46.0",
60
60
  "@vosjs/timeline": "^0.5.0",
61
61
  "@vosjs/tween": "^0.8.3"
62
62
  },
@@ -33,51 +33,14 @@
33
33
  "type": "array",
34
34
  "description": "Run BEFORE the camera rolls, after the first navigation: a sign-in form, a cookie banner, an onboarding tour. 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 selector that never appears fails the take before anything is recorded (exit 2). A `type` step's text may be { \"env\": \"NAME\" }, read at run time and never logged or stored; a literal typed into a password field is refused, because actions.json is committed and pushed.",
35
35
  "items": {
36
- "type": "object",
37
- "required": ["do"],
38
- "additionalProperties": false,
39
- "properties": {
40
- "do": {
41
- "type": "string",
42
- "enum": ["wait", "click", "type", "press", "goto"]
43
- },
44
- "id": {
45
- "type": "string"
46
- },
47
- "selector": {
48
- "type": "string"
49
- },
50
- "text": {
51
- "oneOf": [
52
- {
53
- "type": "string"
54
- },
55
- {
56
- "type": "object",
57
- "required": ["env"],
58
- "additionalProperties": false,
59
- "properties": {
60
- "env": {
61
- "type": "string",
62
- "description": "the environment variable holding the value"
63
- }
64
- }
65
- }
66
- ]
67
- },
68
- "key": {
69
- "type": "string",
70
- "description": "press: a Playwright key name (Enter, Tab, Escape)"
71
- },
72
- "url": {
73
- "type": "string",
74
- "description": "goto: a page to open, for a sign-in that lives elsewhere"
75
- },
76
- "ms": {
77
- "type": "number",
78
- "description": "wait: the pause; click: the read after the page settled, held from its last visual change (default 150); type and scroll: the settle after (defaults 150/200)"
79
- }
80
- }
36
+ "$ref": "#/$defs/offCameraStep"
37
+ }
38
+ },
39
+ "teardown": {
40
+ "type": "array",
41
+ "description": "Run AFTER the take, off camera, with the same verbs and rules as setup: what the take created in the product (an API key, a project, an invite) removed again, so a rehearsal or a re-record leaves the account as it found it. The steps run once the screencast has stopped, on the take's own page and session, after a --dry-run rehearsal too, and also when the take stops early or throws after its first step ran. Nothing of them is captured or written to meta.steps. Bounded at 60 s in all. A step that fails stops the teardown and is reported (the --json done event's `teardown`), and never fails the take or changes its exit code. A { \"env\": \"NAME\" } the shell does not hold refuses the take before it begins.",
42
+ "items": {
43
+ "$ref": "#/$defs/offCameraStep"
81
44
  }
82
45
  },
83
46
  "ready": {
@@ -1107,6 +1070,54 @@
1107
1070
  }
1108
1071
  }
1109
1072
  ]
1073
+ },
1074
+ "offCameraStep": {
1075
+ "type": "object",
1076
+ "required": ["do"],
1077
+ "additionalProperties": false,
1078
+ "properties": {
1079
+ "do": {
1080
+ "type": "string",
1081
+ "enum": ["wait", "click", "type", "press", "goto"]
1082
+ },
1083
+ "id": {
1084
+ "type": "string"
1085
+ },
1086
+ "selector": {
1087
+ "type": "string"
1088
+ },
1089
+ "text": {
1090
+ "oneOf": [
1091
+ {
1092
+ "type": "string"
1093
+ },
1094
+ {
1095
+ "type": "object",
1096
+ "required": ["env"],
1097
+ "additionalProperties": false,
1098
+ "properties": {
1099
+ "env": {
1100
+ "type": "string",
1101
+ "description": "the environment variable holding the value"
1102
+ }
1103
+ }
1104
+ }
1105
+ ]
1106
+ },
1107
+ "key": {
1108
+ "type": "string",
1109
+ "description": "press: a Playwright key name (Enter, Tab, Escape)"
1110
+ },
1111
+ "url": {
1112
+ "type": "string",
1113
+ "description": "goto: a page to open, for a sign-in that lives elsewhere"
1114
+ },
1115
+ "ms": {
1116
+ "type": "number",
1117
+ "description": "wait: the pause; click: the read after the page settled, held from its last visual change (default 150); type and scroll: the settle after (defaults 150/200)"
1118
+ }
1119
+ },
1120
+ "description": "One setup or teardown step: a plain action, with no cursor and no frames."
1110
1121
  }
1111
1122
  }
1112
1123
  }
@@ -334,7 +334,33 @@
334
334
  },
335
335
  "cursor": {
336
336
  "type": "object",
337
- "description": "Cursor rendering (visible, hideWhenIdle, smoothing, size, style, click effects). See CursorStyle in @vosjs/studio-core. `style: \"arrow\"` (the default for new takes) draws an OS-style pointer with its tip on the recorded point; `\"default\"` (every earlier take) draws the white dot. `visible: false` hides the drawn pointer only \u2014 the track still drives cursor-follow zoom, and click effects keep drawing (silence those with clickFx.style: \"none\"). `hideWhenIdle` (default true) fades the dot out after ~1s parked and back in when it moves; scrolling is not movement, and it is always back at full opacity on a click.",
337
+ "description": "Cursor rendering (visible, hideWhenIdle, smoothing, size, style, click effects). See CursorStyle in @vosjs/studio-core. `style: \"arrow\"` (the default for new takes) draws an OS-style pointer with its tip on the recorded point; `\"default\"` (every earlier take) draws the white dot. `visible: false` hides the drawn pointer only \u2014 the track still drives cursor-follow zoom, and click effects keep drawing (silence those with clickFx.style: \"none\"). `hideWhenIdle` (default true) fades the dot out after ~1s parked and back in when it moves; scrolling is not movement, and it is always back at full opacity on a click. `off` hides the drawn pointer over time ranges.",
338
+ "properties": {
339
+ "off": {
340
+ "type": "array",
341
+ "description": "Time ranges in which the drawn pointer is hidden, in OUTPUT seconds (the timeline the viewer sees, like overlays, so a range stays put through trims and speed changes). `{ in, out?, from? }`: an absent `out` hides it to the end of the film. The pointer fades out over 0.15 s before `in` and back in over 0.15 s after `out`, so the range itself is fully hidden; overlapping ranges read as their union. The track still drives cursor-follow zoom and click effects, as with `visible: false`. `from` marks a range something else placed (`endcard`: the end card's, written by `vos plan` and replaced or removed with the end card); a range without one is yours and a re-plan never touches it.",
342
+ "items": {
343
+ "type": "object",
344
+ "required": ["in"],
345
+ "properties": {
346
+ "in": {
347
+ "type": "number",
348
+ "minimum": 0,
349
+ "description": "OUTPUT seconds the pointer is gone from."
350
+ },
351
+ "out": {
352
+ "type": "number",
353
+ "description": "OUTPUT seconds it returns at, past `in`. Absent = to the end of the film."
354
+ },
355
+ "from": {
356
+ "type": "string",
357
+ "description": "What placed it (e.g. `endcard`). Absent = a person's own."
358
+ }
359
+ },
360
+ "additionalProperties": false
361
+ }
362
+ }
363
+ },
338
364
  "additionalProperties": true
339
365
  },
340
366
  "cam": {
package/skills/VERSION CHANGED
@@ -1 +1 @@
1
- 0.26.0
1
+ 0.27.0
@@ -244,3 +244,10 @@ by data. Never spread before the seed is signed off.
244
244
  - A hand-typed dimension: every size comes from `channel-specs.json`.
245
245
  - The save beat on a demo instance that disables writes: say so in
246
246
  `skipped`, never fake the click.
247
+ - A zoom trusted because the track lists it: `frames --at-zooms` renders
248
+ each apex beside its zoom-off twin and names one that matches; read the
249
+ stills before `deliver`, never the list.
250
+ - A rehearsal that presses a step that spends or creates in the shipped
251
+ product: `--dry-run` CLICKS. Guard the spend (`"guard": "spend"` + `id`,
252
+ recorded only with `--confirm <id>`) and write the side-effect ledger
253
+ before the first rehearsal (the `product-video` skill's pre-flight).
@@ -52,6 +52,69 @@ failure / 3 no browser / 4 the recorder met a sign-in instead of the page
52
52
  | **Quick demo** (issue, PR, chat) | 1280×720 | webm, defaults | speed over polish; drafts acceptable here ONLY |
53
53
 
54
54
  Per-channel dimensions and byte budgets: `references/destinations.md`.
55
+ A **how-to** (one flow in a product, from a door to its result, for a docs
56
+ page or a changelog) takes the quick demo's viewport or the launch video's,
57
+ and its own grammar: `references/how-to.md`.
58
+
59
+ ## Pre-flight: six answers before the first take
60
+
61
+ A flow in somebody's product has consequences the footage does not show:
62
+ two rehearsals left two untitled drafts in a founder's projects and both
63
+ appeared in the recorded sidebar; a rehearsal that reaches "Generate"
64
+ spends a credit; a result that arrives twelve minutes later is a second
65
+ take without a plan for it. Answer these BEFORE `vos look`, the script or a
66
+ rehearsal. **Each answer either changes the script or is said to the
67
+ person before recording**; an answer that does neither was not an answer.
68
+
69
+ 1. **The door.** Which way through the product does the video walk, and
70
+ which others exist (a guided flow, a prompt box, an apps menu, a
71
+ template)? A how-to walks ONE. When there are several, list them and
72
+ confirm the door with the person before scripting; a door that works
73
+ under automation beats a recommended one that does not, and hiding the
74
+ automation from the site is never the answer (find another door, or
75
+ hand the shot list to a person: `references/sessions.md`, rung 4).
76
+ 2. **Whose account, face and work.** What the signed-in account shows ships
77
+ in the video: its avatar, its name, its recent items, the titles of its
78
+ other projects. A demo account is the answer; when the person records
79
+ from their own, say what will show before the take. The rehearsal
80
+ reports an address, a key or a card as EXPOSED (mask those), and the
81
+ account's own name and its recent lists as NOTICES: those are the
82
+ person's call. Say they will be on screen and offer the one-line `mask`;
83
+ never mask a name by default, and a face is yours to notice and theirs
84
+ to decide.
85
+ 3. **What the flow CREATES.** Write the side-effect ledger: for each step,
86
+ what it leaves in the product (a draft, a project, a message, a file, a
87
+ row), remembering that `--dry-run` CLICKS, so every rehearsal creates
88
+ them too. Say the ledger to the person before the first rehearsal when
89
+ the product is a real account, and report it at the end so they can
90
+ delete what was made. A product where creating is cheap (a sandbox, a
91
+ seeded local instance) needs the ledger written, not asked.
92
+ 4. **What it SPENDS.** A step that costs something real (a generation, a
93
+ purchase, a message sent, a deploy) carries `"guard": "spend"` and an
94
+ `id`. A rehearsal hovers it and never presses it; `vos record` refuses
95
+ the take before a browser opens unless the command names the step
96
+ (`--confirm <id>`), and the done event records that it was pressed.
97
+ Name the spend to the person in words ("one generation") and get the
98
+ yes before the record; the consent then lives in the file and the
99
+ command line, where a reviewer can read it.
100
+ 5. **Is the result async, and how long.** A product that finishes later (a
101
+ render, a generation, a deploy) is ONE take with an `await` step: the
102
+ capture stops, the page is polled off camera, the capture resumes when
103
+ the predicate holds, and the cut carries the measured time
104
+ (`{elapsed}` in the next caption; `references/how-to.md`). Wait for
105
+ what only the NEW result has (a percentage that appears, then goes;
106
+ never a title an older item shares), size `timeout` from what the
107
+ product measured last time, and `download` the output into
108
+ `take/outputs/` so the join can play it. On the hosted recorder the
109
+ waits come out of the footage cap: say how long before choosing it.
110
+ 6. **Does sound matter.** The recorder captures frames. Sound a page plays
111
+ through a `<video>` or `<audio>` element is kept (`@vosjs/cli` 0.79 and
112
+ later: each heard source lands in `meta.media` and the plan places a
113
+ clip per interval); sound a page synthesizes with Web Audio is not
114
+ heard, and a downloaded output's own sound rides as a clip through
115
+ `vos join`. When the sound IS the point (a voice preview, a generated
116
+ clip) and the product synthesizes it, say so before recording and plan
117
+ the download; when it is not, say the video is silent and score it.
55
118
 
56
119
  ## The core loop (every destination)
57
120
 
@@ -81,6 +144,16 @@ Per-channel dimensions and byte budgets: `references/destinations.md`.
81
144
  take directory and out of git, and record from a demo account: what the
82
145
  account shows ships in the video.
83
146
 
147
+ **Never script a signed-in page blind.** `vos look <url> --session <app>`
148
+ (or `--storage-state`; `@vosjs/cli` 0.78 and later) opens the page
149
+ through the door the recorder will use and prints a screenshot plus
150
+ every control in view with ONE proposed selector each, counted UNIQUE
151
+ or AMBIGUOUS by the recorder's own engine; `--click <selector>` shows
152
+ what a click opens before the script relies on it, and refuses a control
153
+ whose words mean spend, delete or submit. Take UNIQUE selectors into
154
+ steps as they are. Then the side-effect ledger (pre-flight, 3), then
155
+ rehearse.
156
+
84
157
  **Verified the feature with agent-browser already?** Keep that walk and
85
158
  skip the second script. agent-browser's `--json` result does not say
86
159
  what ran (`scroll` answers `{scrolled:true}`), so wrap each call so the
@@ -170,9 +243,13 @@ Per-channel dimensions and byte budgets: `references/destinations.md`.
170
243
  `references/sessions.md`.
171
244
 
172
245
  3. **Record**: `vos record --actions actions.json --out take --strict --json`
173
- `--strict` always: skipped selector / networkidle timeout → exit 2 with
174
- `skipped[]` in the done event. A skip means the flow is broken — fix it,
175
- never ship around it. The take auto-encodes and auto-plans.
246
+ `--strict` always: a skipped selector, a failed `expect`, a click on a
247
+ disabled control, a timed-out `await` or a capped take → exit 2 with
248
+ `skipped[]` in the done event (a first load that never goes network-idle
249
+ is a note, not a failure, since `@vosjs/cli` 0.78). A skip means the
250
+ flow is broken — fix it, never ship around it. A guarded step records
251
+ only with `--confirm <id>` (pre-flight, 4). The take auto-encodes and
252
+ auto-plans.
176
253
  (`vos create --actions actions.json out.webm --strict` is the one-shot
177
254
  record+render verb — fine for a quick first pass, but THIS skill's loop
178
255
  reviews frames before rendering, so prefer the separate verbs here.)
@@ -295,3 +372,14 @@ when the ask is a release, not one video.
295
372
  one per gesture): mark the payoff steps and plan `--zooms per-beat`.
296
373
  - Trusting `--strict` for the scene: it proves the steps ran, not that the
297
374
  app holds what the beat shows. Check with `expect` + `js` + `equals`.
375
+ - Trusting a zoom because the track lists it: two of a film's four zooms
376
+ never rendered and the list said they did. `frames --at-zooms` renders
377
+ each apex beside its zoom-off twin and names an apex that matches its
378
+ twin; read the stills, never the list.
379
+ - A rehearsal that presses a step that spends: `--dry-run` CLICKS. A
380
+ spending step carries `"guard": "spend"` and an `id`, so the rehearsal
381
+ hovers it and `record` refuses it without `--confirm <id>`; a step that
382
+ only creates goes in the side-effect ledger before the first rehearsal.
383
+ - Scripting a signed-in product blind: `vos look` first, then the ledger,
384
+ then the rehearsal. Guessing selectors from a public page costs a take
385
+ per guess.
@@ -0,0 +1,78 @@
1
+ # The how-to: one flow, one door, the result first
2
+
3
+ A how-to shows a person how to reach ONE result in a product: "make a
4
+ cinematic shot in HeyGen", "put a video on vos.so". It is the clip a docs
5
+ page, a changelog entry or a support answer embeds, and it is judged by
6
+ whether a viewer can repeat the path. The marketing rules in `taste.md`
7
+ still hold (one idea, open on the money shot, end settled); these are the
8
+ rules that are specific to the genre. They came out of three agent-made
9
+ how-tos and the cuts a person had to redo on them.
10
+
11
+ ## The grammar
12
+
13
+ 1. **One door.** A product usually has several ways to the same result
14
+ (a guided flow, a prompt box, an apps menu, a template). The video walks
15
+ ONE, and the person named it before the script was written (the
16
+ pre-flight in `SKILL.md`). Two doors in one clip read as two videos;
17
+ a door that works under automation beats the recommended one that does
18
+ not (never hide the automation from a site; pick the door that works, or
19
+ hand the shot list to a person).
20
+
21
+ 2. **One caption per step, in the product's own words for the control.**
22
+ The caption names the control the way the product labels it, as a verb
23
+ phrase: "Open Apps", "Choose Cinematic Shots", "Press Generate". Not a
24
+ sentence about the feature, not marketing copy, nothing the footage does
25
+ not show. It sits on the step whose action it describes (`caption` on
26
+ the step in `actions.json`; `vos plan` and `vos join --captions` land
27
+ each one anchored to its step, so a re-record carries it). One caption
28
+ is one beat; a step with no caption is part of the beat before it.
29
+
30
+ 3. **The result is the payoff, and the cold open.** Open on the finished
31
+ result for a beat (a repeated segment at camera rest,
32
+ `segments[].camera: "rest"`, the `vos-cut` skill's "Assemble"), then
33
+ the path from the door, then the result again, settled, as the last
34
+ frame. The result's step is the beat marked `"zoom": true`; the path's
35
+ clicks stay wide unless one of them is the point. `still` sits on the
36
+ result, never on the door.
37
+
38
+ 4. **A recap card that lists the path.** The last thing on screen is the
39
+ path as a list, in the captions' own words, in order ("Apps → Cinematic
40
+ Shots → Describe the shot → Generate"), on the plate after the card
41
+ fades (`frame.anim.exit: "fade"` plus layers past the footage), never
42
+ over a frozen frame. A viewer who looked away for the middle leaves with
43
+ the path.
44
+
45
+ 5. **Measured times only.** When the product takes time off camera (`await`
46
+ in the script), the cut says how long it took in the words the
47
+ measurement gives: `{elapsed}` in the caption after the wait prints the
48
+ measured time ("8 minutes later, the shot is ready"), or write the
49
+ number read from `meta.skips[].ms` ("About 8 minutes later"). Never a
50
+ round number nobody measured, never "instantly" or "in seconds", and
51
+ never a speed read off a capture (the recorder loads the page too).
52
+
53
+ 6. **Music lands on the result.** `vos score --track <slug> --at <result
54
+ step id>` puts the track's swell on the payoff; a how-to with a voice
55
+ or the product's own sound (a generated video playing) keeps the bed
56
+ under it (`--gain`). The downloaded output's own sound rides as a clip
57
+ through `vos join`.
58
+
59
+ 7. **As long as its path.** A how-to is not held to the 30 s marketing
60
+ band; it is held to its beats: about 1 s of reading after a page
61
+ changes, 0.6 s after a control, the waits cut out, nothing padded.
62
+ Run 3's how-to was 36 s for seven steps and an 8-minute generation.
63
+
64
+ ## The checks, in order
65
+
66
+ ```bash
67
+ vos validate actions.json # the captions sit on the steps they describe
68
+ vos record --actions actions.json --out take --dry-run # the door opens, the guard hovers, the wait is timed
69
+ vos record --actions actions.json --out take --strict --json --confirm <spend id>
70
+ vos plan take --zooms per-beat --still <result output s>
71
+ vos frames take --at-zooms # every apex beside its zoom-off twin
72
+ vos frames take --times <each caption start + 1> # every caption on its own ground, in the product's words
73
+ vos check-video out.mp4 # one cut per 5 s at most, the first and last frames stand alone
74
+ ```
75
+
76
+ A caption the footage does not show, a time that was not measured, a second
77
+ door, or a result that is not the first and last frame each send the cut
78
+ back a round, not the recording.
@@ -25,6 +25,10 @@ The rubric for judging a take before its render ships. Judge stills
25
25
  Launch videos may run longer but every scene must still earn its seconds.
26
26
  3. **End settled.** Close on a resolved state (result visible, motion parked),
27
27
  never mid-action — the loop point should read as intentional.
28
+ A **how-to** (one flow to one result, for a docs page or a changelog)
29
+ keeps all three and adds its own: one door, one caption per step in
30
+ the product's words for the control, the result as the cold open and
31
+ the last frame, a recap card, measured times only (`how-to.md`).
28
32
 
29
33
  ## Flow authoring (actions.json)
30
34