@vosjs/cli 0.34.0 → 0.36.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
@@ -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
 
@@ -207,6 +207,7 @@ import {
207
207
  TEXT_ENTER_KINDS,
208
208
  TEXT_EXIT_KINDS,
209
209
  TILT_DEG_MAX,
210
+ RETIRED_ZOOM_STYLES,
210
211
  TILT_SPAN_MIN,
211
212
  ZOOM_LEVEL_MAX,
212
213
  ZOOM_LEVEL_MIN,
@@ -666,6 +667,12 @@ function lintDoc(docIn) {
666
667
  );
667
668
  }
668
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
+ }
669
676
  if (doc.tiltStyle !== void 0 && !["off", "subtle", "medium", "strong"].includes(doc.tiltStyle)) {
670
677
  problems.push(
671
678
  `tiltStyle must be "off" | "subtle" | "medium" | "strong" (got ${String(doc.tiltStyle)})`
@@ -3869,9 +3876,9 @@ function proposeTransitions(input, kind) {
3869
3876
  for (const step of steps) {
3870
3877
  if (!step.navigated || step.skipped) continue;
3871
3878
  const name = step.id ?? String(step.step);
3872
- const wait = step.do === "wait";
3873
- const outEnd = round3(wait ? step.tStart : step.tEnd);
3874
- const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
3879
+ const wait2 = step.do === "wait";
3880
+ const outEnd = round3(wait2 ? step.tStart : step.tEnd);
3881
+ const inStart = round3(wait2 ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
3875
3882
  const segs = doc.segments;
3876
3883
  if (segs.some(
3877
3884
  (s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
@@ -6158,8 +6165,8 @@ async function clockMotion(dur, at2, clock) {
6158
6165
  await at2(easeInOutCubic(Math.min(1, elapsed / dur)));
6159
6166
  samples++;
6160
6167
  const next = start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS;
6161
- const wait = Math.min(next, start + dur) - clock.now();
6162
- if (wait > 0) await clock.sleep(wait);
6168
+ const wait2 = Math.min(next, start + dur) - clock.now();
6169
+ if (wait2 > 0) await clock.sleep(wait2);
6163
6170
  }
6164
6171
  await at2(1);
6165
6172
  return samples + 1;
@@ -6169,8 +6176,8 @@ async function clockTyping(chars, delay, type, clock) {
6169
6176
  for (let i = 0; i < chars.length; i++) {
6170
6177
  await type(chars[i], i);
6171
6178
  const due = start + (i + 1) * delay;
6172
- const wait = due - clock.now();
6173
- if (wait > 0 && i < chars.length - 1) await clock.sleep(wait);
6179
+ const wait2 = due - clock.now();
6180
+ if (wait2 > 0 && i < chars.length - 1) await clock.sleep(wait2);
6174
6181
  }
6175
6182
  }
6176
6183
  function askedMs(step) {
@@ -7378,7 +7385,7 @@ async function cmdMove(argv) {
7378
7385
  });
7379
7386
  const key = requireCredential(strFlag(flags, "key"));
7380
7387
  const folderId = toRef === "none" ? null : resolveFolder(await listFolders(origin, key), toRef).id;
7381
- let failed = 0;
7388
+ let failed2 = 0;
7382
7389
  const moved = [];
7383
7390
  for (const raw of positionals) {
7384
7391
  const id = parseVosId(raw);
@@ -7393,28 +7400,28 @@ async function cmdMove(argv) {
7393
7400
  continue;
7394
7401
  }
7395
7402
  if (asVos.status === 404) {
7396
- const asAsset = await apiJson(origin, `/api/assets/${id}`, {
7403
+ const asAsset2 = await apiJson(origin, `/api/assets/${id}`, {
7397
7404
  method: "PATCH",
7398
7405
  key,
7399
7406
  body: { folderId }
7400
7407
  });
7401
- if (asAsset.status === 200) {
7408
+ if (asAsset2.status === 200) {
7402
7409
  moved.push({ id, kind: "asset" });
7403
7410
  r.log(`moved asset ${id}`);
7404
7411
  continue;
7405
7412
  }
7406
- failed++;
7407
- r.log(apiError(`move ${id}`, asAsset));
7413
+ failed2++;
7414
+ r.log(apiError(`move ${id}`, asAsset2));
7408
7415
  continue;
7409
7416
  }
7410
- failed++;
7417
+ failed2++;
7411
7418
  r.log(apiError(`move ${id}`, asVos));
7412
7419
  }
7413
7420
  r.done(
7414
- { moved, failed, folderId },
7415
- `moved ${moved.length}/${positionals.length} into ${toRef === "none" ? "the top level" : toRef}${failed ? ` \u2014 ${failed} failed` : ""}`
7421
+ { moved, failed: failed2, folderId },
7422
+ `moved ${moved.length}/${positionals.length} into ${toRef === "none" ? "the top level" : toRef}${failed2 ? ` \u2014 ${failed2} failed` : ""}`
7416
7423
  );
7417
- return failed ? EXIT_ERROR : EXIT_OK;
7424
+ return failed2 ? EXIT_ERROR : EXIT_OK;
7418
7425
  }
7419
7426
  function recipeLine(filename, body) {
7420
7427
  const h = recipeHints(body);
@@ -7560,6 +7567,144 @@ async function cmdFolder(argv) {
7560
7567
  }
7561
7568
  }
7562
7569
 
7570
+ // src/plugin/uploadAsset.ts
7571
+ var SINGLE_SHOT_MAX_BYTES = 32 * 1024 * 1024;
7572
+ var PART_ATTEMPTS = 3;
7573
+ function planParts(totalBytes, partBytes) {
7574
+ if (totalBytes <= 0 || partBytes <= 0) return [];
7575
+ const parts = [];
7576
+ for (let start = 0; start < totalBytes; start += partBytes) {
7577
+ parts.push({
7578
+ partNumber: parts.length + 1,
7579
+ start,
7580
+ end: Math.min(start + partBytes, totalBytes)
7581
+ });
7582
+ }
7583
+ return parts;
7584
+ }
7585
+ function failed(what, r) {
7586
+ const detail = typeof r.body.error === "string" ? r.body.error : "";
7587
+ return new Error(`${what} (${r.status})${detail ? `: ${detail}` : ""}`);
7588
+ }
7589
+ function asAsset(body) {
7590
+ return {
7591
+ id: String(body.id),
7592
+ url: String(body.url),
7593
+ size: typeof body.size === "number" ? body.size : 0,
7594
+ reused: body.reused === true
7595
+ };
7596
+ }
7597
+ function transient(status) {
7598
+ return status >= 500 || status === 429;
7599
+ }
7600
+ var wait = (ms) => new Promise((r) => setTimeout(r, ms));
7601
+ async function uploadAsset(target, bytes, opts) {
7602
+ return bytes.length > SINGLE_SHOT_MAX_BYTES ? uploadInParts(target, bytes, opts) : uploadWhole(target, bytes, opts);
7603
+ }
7604
+ async function uploadWhole(target, bytes, opts) {
7605
+ const res = await apiJson(target.origin, "/api/assets/recording", {
7606
+ method: "POST",
7607
+ key: target.key,
7608
+ headers: {
7609
+ "Content-Type": opts.contentType,
7610
+ "Content-Length": String(bytes.length),
7611
+ "X-Filename": opts.filename,
7612
+ "X-Content-Hash": opts.contentHash,
7613
+ ...opts.durationSeconds && opts.durationSeconds > 0 ? { "X-Content-Duration": opts.durationSeconds.toFixed(3) } : {}
7614
+ },
7615
+ raw: bytes
7616
+ });
7617
+ if (res.status !== 201 && res.status !== 200)
7618
+ throw failed("upload failed", res);
7619
+ return asAsset(res.body);
7620
+ }
7621
+ async function uploadInParts(target, bytes, opts) {
7622
+ const begun = await apiJson(
7623
+ target.origin,
7624
+ "/api/assets/recording/multipart",
7625
+ {
7626
+ method: "POST",
7627
+ key: target.key,
7628
+ body: {
7629
+ filename: opts.filename,
7630
+ contentType: opts.contentType,
7631
+ size: bytes.length,
7632
+ contentHash: opts.contentHash,
7633
+ ...opts.durationSeconds && opts.durationSeconds > 0 ? { durationSeconds: opts.durationSeconds } : {}
7634
+ }
7635
+ }
7636
+ );
7637
+ if (begun.status !== 201 && begun.status !== 200) {
7638
+ throw failed("upload failed", begun);
7639
+ }
7640
+ if (begun.body.reused === true || typeof begun.body.url === "string") {
7641
+ return asAsset(begun.body);
7642
+ }
7643
+ const uploadId = String(begun.body.id);
7644
+ const partBytes = Number(begun.body.partBytes);
7645
+ if (!Number.isFinite(partBytes) || partBytes <= 0) {
7646
+ throw new Error("upload failed: the platform returned no part size");
7647
+ }
7648
+ const parts = planParts(bytes.length, partBytes);
7649
+ const etags = [];
7650
+ try {
7651
+ for (const part of parts) {
7652
+ const chunk2 = bytes.subarray(part.start, part.end);
7653
+ let sent = null;
7654
+ for (let attempt = 1; attempt <= PART_ATTEMPTS; attempt++) {
7655
+ try {
7656
+ sent = await apiJson(
7657
+ target.origin,
7658
+ `/api/assets/recording/multipart/${uploadId}/parts/${part.partNumber}`,
7659
+ {
7660
+ method: "PUT",
7661
+ key: target.key,
7662
+ headers: {
7663
+ "Content-Type": "application/octet-stream",
7664
+ "Content-Length": String(chunk2.length)
7665
+ },
7666
+ raw: chunk2
7667
+ }
7668
+ );
7669
+ } catch (networkError) {
7670
+ if (attempt === PART_ATTEMPTS) throw networkError;
7671
+ await wait(500 * 2 ** (attempt - 1));
7672
+ continue;
7673
+ }
7674
+ if (sent.status === 200) break;
7675
+ if (!transient(sent.status) || attempt === PART_ATTEMPTS) {
7676
+ throw failed(`part ${part.partNumber} failed`, sent);
7677
+ }
7678
+ await wait(500 * 2 ** (attempt - 1));
7679
+ }
7680
+ if (!sent || sent.status !== 200) {
7681
+ throw new Error(`part ${part.partNumber} failed`);
7682
+ }
7683
+ etags.push({
7684
+ partNumber: Number(sent.body.partNumber),
7685
+ etag: String(sent.body.etag)
7686
+ });
7687
+ opts.onPart?.(etags.length, parts.length);
7688
+ }
7689
+ const sealed = await apiJson(
7690
+ target.origin,
7691
+ `/api/assets/recording/multipart/${uploadId}/complete`,
7692
+ { method: "POST", key: target.key, body: { parts: etags } }
7693
+ );
7694
+ if (sealed.status !== 201 && sealed.status !== 200) {
7695
+ throw failed("upload failed", sealed);
7696
+ }
7697
+ return asAsset(sealed.body);
7698
+ } catch (err) {
7699
+ await apiJson(
7700
+ target.origin,
7701
+ `/api/assets/recording/multipart/${uploadId}`,
7702
+ { method: "DELETE", key: target.key }
7703
+ ).catch(() => void 0);
7704
+ throw err;
7705
+ }
7706
+ }
7707
+
7563
7708
  // src/plugin/sync.ts
7564
7709
  function apiContext(flags) {
7565
7710
  return {
@@ -7642,31 +7787,25 @@ async function pushTake(dir, flags, r) {
7642
7787
  const bytes = await readFile13(take.paths.recording);
7643
7788
  const hash = createHash("sha256").update(bytes).digest("hex");
7644
7789
  r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
7645
- const upload = await api(ctx, "/assets/recording", {
7646
- method: "POST",
7647
- headers: {
7648
- "Content-Type": "video/webm",
7649
- "Content-Length": String(bytes.length),
7650
- "X-Filename": RECORDING_NAME,
7651
- "X-Content-Hash": hash,
7790
+ let upload;
7791
+ try {
7792
+ upload = await uploadAsset(ctx, new Uint8Array(bytes), {
7793
+ filename: RECORDING_NAME,
7794
+ contentType: "video/webm",
7795
+ contentHash: hash,
7652
7796
  // The take's length so the server holds it to the plan's cap.
7653
- ...take.meta.durationMs > 0 ? { "X-Content-Duration": (take.meta.durationMs / 1e3).toFixed(3) } : {}
7654
- },
7655
- raw: new Uint8Array(bytes)
7656
- });
7657
- if (upload.status !== 201 && upload.status !== 200) {
7797
+ ...take.meta.durationMs > 0 ? { durationSeconds: take.meta.durationMs / 1e3 } : {},
7798
+ onPart: (done, total) => r.log(` part ${done}/${total}`)
7799
+ });
7800
+ } catch (e) {
7658
7801
  throw new Error(
7659
- `recording upload failed (${upload.status}): ${String(upload.json.error ?? "")}`
7802
+ `recording upload failed: ${e instanceof Error ? e.message : String(e)}`
7660
7803
  );
7661
7804
  }
7662
- const assetId = String(upload.json.id);
7663
- const assetUrl = String(upload.json.url);
7664
- r.event({
7665
- event: "recording",
7666
- assetId,
7667
- reused: upload.json.reused === true
7668
- });
7669
- if (upload.json.reused === true) r.log(" recording already hosted \u2014 reused");
7805
+ const assetId = upload.id;
7806
+ const assetUrl = upload.url;
7807
+ r.event({ event: "recording", assetId, reused: upload.reused });
7808
+ if (upload.reused) r.log(" recording already hosted \u2014 reused");
7670
7809
  const docForPush = structuredClone(take.doc);
7671
7810
  docForPush.source.videoKey = assetUrl;
7672
7811
  if (docForPush.source.camKey && !/^(https?:)?\//.test(docForPush.source.camKey)) {
@@ -7686,24 +7825,21 @@ async function pushTake(dir, flags, r) {
7686
7825
  const media = await readFile13(file);
7687
7826
  const mediaHash = createHash("sha256").update(media).digest("hex");
7688
7827
  const type = mediaContentType(file);
7689
- const put = await api(ctx, "/assets/recording", {
7690
- method: "POST",
7691
- headers: {
7692
- "Content-Type": type,
7693
- "Content-Length": String(media.length),
7694
- "X-Filename": basename(file),
7695
- "X-Content-Hash": mediaHash
7696
- },
7697
- raw: new Uint8Array(media)
7698
- });
7699
- if (put.status !== 201 && put.status !== 200) {
7828
+ let put;
7829
+ try {
7830
+ put = await uploadAsset(ctx, new Uint8Array(media), {
7831
+ filename: basename(file),
7832
+ contentType: type,
7833
+ contentHash: mediaHash
7834
+ });
7835
+ } catch (e) {
7700
7836
  throw new Error(
7701
- `${ref.where} upload failed (${put.status}): ${String(put.json.error ?? "")}`
7837
+ `${ref.where} upload failed: ${e instanceof Error ? e.message : String(e)}`
7702
7838
  );
7703
7839
  }
7704
- ref.set(String(put.json.url));
7840
+ ref.set(put.url);
7705
7841
  r.log(
7706
- ` ${ref.where}: ${ref.key} \u2192 asset ${String(put.json.id)}${put.json.reused === true ? " (reused)" : ""}`
7842
+ ` ${ref.where}: ${ref.key} \u2192 asset ${put.id}${put.reused ? " (reused)" : ""}`
7707
7843
  );
7708
7844
  }
7709
7845
  const lowered = lowerToComposition3(docForPush);
@@ -11239,4 +11375,4 @@ export {
11239
11375
  convertAgentBrowser,
11240
11376
  run
11241
11377
  };
11242
- //# sourceMappingURL=chunk-43FCXWNS.js.map
11378
+ //# sourceMappingURL=chunk-MIZWK5AN.js.map