@mevdragon/vidfarm-devcli 0.20.4 → 0.20.6

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.
@@ -1550,10 +1550,47 @@ Return strictly valid JSON matching this schema:
1550
1550
  "arc": string,
1551
1551
  "peak_moment": string,
1552
1552
  "mechanism": string,
1553
+ "contrast": string,
1553
1554
  "humor": string,
1555
+ "audio_visual_tension": string,
1554
1556
  "delivery": string,
1555
1557
  "preserve": [string, ...]
1556
1558
  },
1559
+ "static_vs_pivot": {
1560
+ "summary": string,
1561
+ "scene_replacement": {
1562
+ "overall": "replace_most" | "replace_some" | "keep_most" | "keep_all",
1563
+ "reason": string,
1564
+ "must_keep": [string, ...],
1565
+ "should_replace": [string, ...],
1566
+ "load_bearing_scenes": [string, ...]
1567
+ },
1568
+ "narration": {
1569
+ "overall": "keep_original" | "customize_script" | "optional",
1570
+ "reason": string,
1571
+ "same_timing_required": boolean,
1572
+ "use_premium_tts_when_customizing": boolean
1573
+ },
1574
+ "music": {
1575
+ "overall": "keep_original" | "recreate_under_narration" | "optional" | "none",
1576
+ "reason": string,
1577
+ "volume_guidance": string
1578
+ },
1579
+ "captions": {
1580
+ "overall": "reuse_existing" | "retime_to_new_narration" | "optional" | "none",
1581
+ "reason": string,
1582
+ "animation_dependency": string
1583
+ },
1584
+ "visual_rebuild": {
1585
+ "default_strategy": "ai_images_with_ken_burns" | "real_footage" | "mixed",
1586
+ "reason": string
1587
+ },
1588
+ "viral_dna": {
1589
+ "must_keep": [string, ...],
1590
+ "flexible": [string, ...],
1591
+ "do_not_change": [string, ...]
1592
+ }
1593
+ },
1557
1594
  "preserve": [string, ...],
1558
1595
  "avoid": [string, ...],
1559
1596
  "promotions": [string, ...],
@@ -1598,10 +1635,20 @@ VIRAL_DNA RULES:
1598
1635
  - "arc": how the feeling MOVES across the runtime (e.g. "curiosity → mounting tension → relief-laugh", "flat setup → sudden absurd turn").
1599
1636
  - "peak_moment": the single beat that lands the emotional hit — the punchline, the reveal, the gut-punch, the beat-drop — and roughly when.
1600
1637
  - "mechanism": WHY it lands — the technique doing the work (comedic timing, irony, subversion of expectation, escalation, deadpan intonation, relatability, a trending-sound drop synced to the visual). Weigh the AUDIO here: on TikTok/Reels the trending sound or music bed is frequently the single biggest driver of the emotional hit, so if a recognizable trending sound, a beat drop, or a music swell is doing the work, name it as the mechanism.
1601
- - "humor": if it's funny, state what the JOKE actually is and why it works, concretely enough that a remixer can rebuild the same joke with new subject matter; write "none" if the format is not comedic.
1638
+ - "contrast": the specific mismatch / juxtaposition that creates the feeling. This is CRITICAL for irony- or sass-driven formats: identify the actual tension, not just the visible gesture. Examples: "hard, macho soundtrack over a dainty office-laptop scene", "overconfident delivery about a trivial preference", "luxury attitude applied to a mundane workflow". If there is no real mismatch, write "none".
1639
+ - "humor": if it's funny, state what the JOKE actually is and why it works, concretely enough that a remixer can rebuild the same joke with new subject matter; write "none" if the format is not comedic. Focus on the underlying bit, not surface choreography.
1640
+ - "audio_visual_tension": describe how the SOUND and VISUALS interact emotionally: whether the soundtrack / vocal cadence reinforces the visuals, undercuts them, or creates a deliberate mismatch. Name the exact pairing if that's the joke or swagger engine; write "none" only if audio contributes nothing.
1602
1641
  - "delivery": the performance cues to preserve so the vibe survives — speech intonation / cadence (from the AUDIO TRANSCRIPT), the pause held before the punchline, a facial beat or reaction, and especially the SOUND: the music / trending-audio bed, where the beat drop or lyric hit lands relative to the cut, a music swell, an sfx sting, the deadpan-then-cut. This is what keeps intonation, comedic timing, and the audio-driven vibe from being lost in a remix.
1603
1642
  - "preserve": 2-4 emotional beats a remix MUST keep to reproduce the same feeling (e.g. "keep the beat drop landing exactly on the reveal cut", "hold the silent beat before the reveal", "keep the flat unbothered delivery", "the payoff must undercut the buildup, not confirm it").
1604
- Use BOTH the frames and the AUDIO TRANSCRIPT block above. Never leave emotional_punch empty — infer the feeling from the frames, captions, and pacing even when there is no audio, and when the audio is music-only, treat the sound itself as the emotional engine.
1643
+ Use BOTH the frames and the AUDIO TRANSCRIPT block above. Never leave emotional_punch empty — infer the feeling from the frames, captions, and pacing even when there is no audio, and when the audio is music-only, treat the sound itself as the emotional engine. Do NOT over-index on a single gesture or hand movement if the real emotional engine is the character attitude, the soundtrack choice, the setting mismatch, or the audio-vs-visual contrast.
1644
+ - "static_vs_pivot": this is the load-bearing reuse guidance for lazy prompts like "please recreate this video for my business". Make it operational, not vague.
1645
+ - "summary": one sentence on what can change versus what must stay.
1646
+ - "scene_replacement": decide whether most scenes should be replaced or only some, why, which scenes are load-bearing, and which scenes are the first candidates to swap.
1647
+ - "narration": decide whether the original spoken audio can stay as-is (for example because it is a generic or trending sound) or whether it must be customized because the words are source-specific. Set "customize_script" whenever the spoken words would stop making sense after a subject swap. If narration is timing-sensitive, set same_timing_required true.
1648
+ - "music": decide whether original music should stay, be recreated under a new narration, or is irrelevant. Include simple mix guidance like "keep low under VO" or "music is the main viral engine".
1649
+ - "captions": decide whether existing caption timing/animation can be reused, whether they must be re-timed to new narration, or whether captions are not important. Call out if the animation is tightly coupled to the spoken words.
1650
+ - "visual_rebuild": default strategy for replacement visuals. When the source mainly uses graphics, screenshots, product cards, or still-like scenes that do not need expensive motion video, prefer "ai_images_with_ken_burns" and explain that slow pan/zoom is enough.
1651
+ - "viral_dna": separate format elements into "must_keep", "flexible", and "do_not_change" so an agent can preserve the viral structure while swapping subject matter.
1605
1652
  - "preserve": 3-5 concrete elements a remixer MUST keep to preserve the format (e.g., "Pinned comment stays on-screen the whole video", "Wedding dance payoff shot").
1606
1653
  - "avoid": 3-5 things that would break the format if changed (e.g., "Do not remove the on-screen text overlay", "Do not slow the cuts below 1.5s").
1607
1654
  - "promotions": 3-5 categories of product / service / offer this exact format could be reused to promote, WITHOUT changing the beat structure or hook mechanic. Each item is a concrete pitchable use-case, not a genre. Examples: "SaaS free-trial signup (before/after workflow)", "Fitness challenge (7-day transformation)", "Local restaurant grand-opening (line-out-the-door proof)", "Ebook / course launch (result-first payoff)", "Real-estate open-house (walk-through reveal)". Anchor each promotion to the same viral tactic captured in "hook" — do not invent unrelated verticals.
@@ -1781,10 +1828,36 @@ function resolveFontSize(input) {
1781
1828
  return normalizeFontSize(input.fontSize, input.canvasHeight);
1782
1829
  }
1783
1830
  const DEFAULT_EMOTIONAL_PUNCH = {
1784
- core_emotion: "", tone: "", arc: "", peak_moment: "", mechanism: "", humor: "", delivery: "", preserve: []
1831
+ core_emotion: "",
1832
+ tone: "",
1833
+ arc: "",
1834
+ peak_moment: "",
1835
+ mechanism: "",
1836
+ contrast: "",
1837
+ humor: "",
1838
+ audio_visual_tension: "",
1839
+ delivery: "",
1840
+ preserve: []
1785
1841
  };
1786
1842
  export const DEFAULT_VIRAL_DNA = {
1787
- trend_tagline: "", hook: "", retention: "", payoff: "", emotional_punch: { ...DEFAULT_EMOTIONAL_PUNCH }, preserve: [], avoid: [], promotions: [], keywords: []
1843
+ trend_tagline: "",
1844
+ hook: "",
1845
+ retention: "",
1846
+ payoff: "",
1847
+ emotional_punch: { ...DEFAULT_EMOTIONAL_PUNCH },
1848
+ static_vs_pivot: {
1849
+ summary: "",
1850
+ scene_replacement: { overall: "replace_some", reason: "", must_keep: [], should_replace: [], load_bearing_scenes: [] },
1851
+ narration: { overall: "optional", reason: "", same_timing_required: false, use_premium_tts_when_customizing: true },
1852
+ music: { overall: "optional", reason: "", volume_guidance: "" },
1853
+ captions: { overall: "optional", reason: "", animation_dependency: "" },
1854
+ visual_rebuild: { default_strategy: "mixed", reason: "" },
1855
+ viral_dna: { must_keep: [], flexible: [], do_not_change: [] }
1856
+ },
1857
+ preserve: [],
1858
+ avoid: [],
1859
+ promotions: [],
1860
+ keywords: []
1788
1861
  };
1789
1862
  function normalizeEmotionalPunch(value) {
1790
1863
  if (!value || typeof value !== "object")
@@ -1800,11 +1873,89 @@ function normalizeEmotionalPunch(value) {
1800
1873
  arc: asString(record.arc ?? record.emotional_arc),
1801
1874
  peak_moment: asString(record.peak_moment ?? record.peakMoment),
1802
1875
  mechanism: asString(record.mechanism),
1876
+ contrast: asString(record.contrast),
1803
1877
  humor: asString(record.humor),
1878
+ audio_visual_tension: asString(record.audio_visual_tension ?? record.audioVisualTension),
1804
1879
  delivery: asString(record.delivery),
1805
1880
  preserve: asStringArray(record.preserve).slice(0, 4)
1806
1881
  };
1807
1882
  }
1883
+ function normalizeStaticVsPivot(value) {
1884
+ const fallback = DEFAULT_VIRAL_DNA.static_vs_pivot;
1885
+ if (!value || typeof value !== "object") {
1886
+ return {
1887
+ ...fallback,
1888
+ scene_replacement: { ...fallback.scene_replacement },
1889
+ narration: { ...fallback.narration },
1890
+ music: { ...fallback.music },
1891
+ captions: { ...fallback.captions },
1892
+ visual_rebuild: { ...fallback.visual_rebuild },
1893
+ viral_dna: { ...fallback.viral_dna }
1894
+ };
1895
+ }
1896
+ const record = value;
1897
+ const asString = (v) => typeof v === "string" ? v.trim() : "";
1898
+ const asStringArray = (v) => Array.isArray(v)
1899
+ ? v.map((item) => (typeof item === "string" ? item.trim() : "")).filter((item) => item.length > 0)
1900
+ : [];
1901
+ const sceneReplacement = record.scene_replacement && typeof record.scene_replacement === "object"
1902
+ ? record.scene_replacement
1903
+ : {};
1904
+ const narration = record.narration && typeof record.narration === "object"
1905
+ ? record.narration
1906
+ : {};
1907
+ const music = record.music && typeof record.music === "object"
1908
+ ? record.music
1909
+ : {};
1910
+ const captions = record.captions && typeof record.captions === "object"
1911
+ ? record.captions
1912
+ : {};
1913
+ const visualRebuild = record.visual_rebuild && typeof record.visual_rebuild === "object"
1914
+ ? record.visual_rebuild
1915
+ : {};
1916
+ const viralDna = record.viral_dna && typeof record.viral_dna === "object"
1917
+ ? record.viral_dna
1918
+ : {};
1919
+ const oneOf = (v, allowed, fallbackValue) => {
1920
+ const text = asString(v);
1921
+ return allowed.includes(text) ? text : fallbackValue;
1922
+ };
1923
+ return {
1924
+ summary: asString(record.summary),
1925
+ scene_replacement: {
1926
+ overall: oneOf(sceneReplacement.overall, ["replace_most", "replace_some", "keep_most", "keep_all"], "replace_some"),
1927
+ reason: asString(sceneReplacement.reason),
1928
+ must_keep: asStringArray(sceneReplacement.must_keep),
1929
+ should_replace: asStringArray(sceneReplacement.should_replace),
1930
+ load_bearing_scenes: asStringArray(sceneReplacement.load_bearing_scenes)
1931
+ },
1932
+ narration: {
1933
+ overall: oneOf(narration.overall, ["keep_original", "customize_script", "optional"], "optional"),
1934
+ reason: asString(narration.reason),
1935
+ same_timing_required: narration.same_timing_required === true,
1936
+ use_premium_tts_when_customizing: narration.use_premium_tts_when_customizing !== false
1937
+ },
1938
+ music: {
1939
+ overall: oneOf(music.overall, ["keep_original", "recreate_under_narration", "optional", "none"], "optional"),
1940
+ reason: asString(music.reason),
1941
+ volume_guidance: asString(music.volume_guidance)
1942
+ },
1943
+ captions: {
1944
+ overall: oneOf(captions.overall, ["reuse_existing", "retime_to_new_narration", "optional", "none"], "optional"),
1945
+ reason: asString(captions.reason),
1946
+ animation_dependency: asString(captions.animation_dependency)
1947
+ },
1948
+ visual_rebuild: {
1949
+ default_strategy: oneOf(visualRebuild.default_strategy, ["ai_images_with_ken_burns", "real_footage", "mixed"], "mixed"),
1950
+ reason: asString(visualRebuild.reason)
1951
+ },
1952
+ viral_dna: {
1953
+ must_keep: asStringArray(viralDna.must_keep),
1954
+ flexible: asStringArray(viralDna.flexible),
1955
+ do_not_change: asStringArray(viralDna.do_not_change)
1956
+ }
1957
+ };
1958
+ }
1808
1959
  function normalizeViralDna(value) {
1809
1960
  if (!value || typeof value !== "object")
1810
1961
  return DEFAULT_VIRAL_DNA;
@@ -1819,6 +1970,7 @@ function normalizeViralDna(value) {
1819
1970
  retention: asString(record.retention),
1820
1971
  payoff: asString(record.payoff),
1821
1972
  emotional_punch: normalizeEmotionalPunch(record.emotional_punch ?? record.emotionalPunch),
1973
+ static_vs_pivot: normalizeStaticVsPivot(record.static_vs_pivot ?? record.staticVsPivot),
1822
1974
  preserve: asStringArray(record.preserve),
1823
1975
  avoid: asStringArray(record.avoid),
1824
1976
  promotions: asStringArray(record.promotions),
@@ -1865,12 +2017,26 @@ function buildEditorHarnessPrompt(input) {
1865
2017
  const roleVocab = SCENE_ROLE_VOCAB.join(" | ");
1866
2018
  const formatVocab = CONTENT_FORMATS.join(" | ");
1867
2019
  const audioDigest = buildAudioDigestForPrompt(input.transcript ?? null);
2020
+ const emotional = input.viralDna?.emotional_punch;
2021
+ const emotionalSnapshot = emotional && [
2022
+ emotional.core_emotion,
2023
+ emotional.tone,
2024
+ emotional.arc,
2025
+ emotional.peak_moment,
2026
+ emotional.mechanism,
2027
+ emotional.contrast,
2028
+ emotional.humor,
2029
+ emotional.audio_visual_tension,
2030
+ emotional.delivery
2031
+ ].some((value) => value && value.trim().length > 0)
2032
+ ? `\nVIRAL DNA FEELING SNAPSHOT (operationalize this in the harness; do NOT drift from it):\n- core_emotion: ${emotional.core_emotion || "—"}\n- tone: ${emotional.tone || "—"}\n- arc: ${emotional.arc || "—"}\n- peak_moment: ${emotional.peak_moment || "—"}\n- mechanism: ${emotional.mechanism || "—"}\n- contrast: ${emotional.contrast || "—"}\n- humor: ${emotional.humor || "—"}\n- audio_visual_tension: ${emotional.audio_visual_tension || "—"}\n- delivery: ${emotional.delivery || "—"}\n- preserve: ${(emotional.preserve || []).join("; ") || "—"}\n`
2033
+ : "";
1868
2034
  return `You are Vidfarm's EDITING DIRECTOR. You are studying a short-form video to write an EDITOR HARNESS — a compact, technical brief that lets another AI editor recreate this video's STYLE (not its exact content) in one clean pass on a timeline editor (scenes, captions, audio, transitions, keyframes).
1869
2035
 
1870
2036
  This is DIFFERENT from "viral DNA" (why the video works / what to preserve). The harness is HOW to edit: pace, typography, b-roll usage, transitions, audio bed, and the role each scene plays. Be concrete and directive — every field should be something an editor can act on, not vibes.
1871
2037
 
1872
2038
  Video duration: ${input.durationSeconds.toFixed(2)} seconds.
1873
- ${frameListing}${cutListing}${audioDigest}${promptHint}
2039
+ ${frameListing}${cutListing}${audioDigest}${emotionalSnapshot}${promptHint}
1874
2040
  Return strictly valid JSON matching this schema:
1875
2041
  {
1876
2042
  "format": string, // one of: ${formatVocab}
@@ -1933,7 +2099,7 @@ RULES:
1933
2099
  - Ground pacing numbers on the detected cuts + duration when cuts are supplied; otherwise estimate from the frames. Round avg_scene_seconds and cuts_per_10s to at most 2 decimals.
1934
2100
  - "scenes": 3-8 entries covering the whole runtime in order, each tied to a real moment you see. Reserve importance "critical" for the 1-3 beats that actually carry the style/retention (usually the hook + payoff).
1935
2101
  - Prefer the caption_style / transition / shot_kind vocabulary above so the editor can map straight to presets; a free-form label is fine when nothing fits.
1936
- - "emotional": this is HOW to edit so the FEELING survives — the vibe, the joke, the intonation, and the SOUND are the first things a remix flattens, so make these directive. Use the AUDIO TRANSCRIPT block: on TikTok/Reels the trending sound / music bed is often the biggest carrier of the feeling. "target_feeling" = the emotion the recreated edit must produce; "tone" = the register to hold; "comedic_timing" = the exact editing move that sells the joke (the beat held before a cut, the deadpan-then-punch, the sfx sting or beat-drop on the button word) or "none"; "intonation" = the vocal delivery/cadence to preserve on any VO or on-cam speech (or "none" if silent) — flag it if the speech must be re-voiced in the SAME cadence; "vibe_anchors" = 2-4 concrete edit choices that carry the emotion, and include the audio ones (e.g. "keep the trending sound and land the beat drop on the reveal cut", "swell the music on the payoff", "hold a full beat of silence before the reveal cut", "cut hard on the punch word"). Infer the feeling from frames + pacing even without audio, and when the audio is music-only treat the sound as the emotional engine; never leave it empty.
2102
+ - "emotional": this is HOW to edit so the FEELING survives — the vibe, the joke, the intonation, and the SOUND are the first things a remix flattens, so make these directive. Use the AUDIO TRANSCRIPT block: on TikTok/Reels the trending sound / music bed is often the biggest carrier of the feeling. "target_feeling" = the emotion the recreated edit must produce; "tone" = the register to hold; "comedic_timing" = the exact editing move that sells the joke (the beat held before a cut, the deadpan-then-punch, the sfx sting or beat-drop on the button word) or "none"; "intonation" = the vocal delivery/cadence to preserve on any VO or on-cam speech (or "none" if silent) — flag it if the speech must be re-voiced in the SAME cadence; "vibe_anchors" = 2-4 concrete edit choices that carry the emotion, and include the audio ones (e.g. "keep the trending sound and land the beat drop on the reveal cut", "swell the music on the payoff", "hold a full beat of silence before the reveal cut", "cut hard on the punch word"). When the viral-DNA snapshot above names a contrast or audio_visual_tension, convert that into actionable edit moves instead of drifting to superficial gestures. Infer the feeling from frames + pacing even without audio, and when the audio is music-only treat the sound as the emotional engine; never leave it empty.
1937
2103
  - Keep every string tight and directive. Never invent audio you cannot infer — say "none"/"unknown" instead.
1938
2104
  - Output ONLY the JSON object.`;
1939
2105
  }
@@ -2030,7 +2196,8 @@ async function runEditorHarnessPass(input) {
2030
2196
  frameTimestamps: input.frames.map((f) => f.timestamp),
2031
2197
  sceneCuts: input.sceneCuts,
2032
2198
  userPrompt: input.userPrompt ?? null,
2033
- transcript: input.transcript ?? null
2199
+ transcript: input.transcript ?? null,
2200
+ viralDna: input.viralDna ?? null
2034
2201
  });
2035
2202
  const attempt = await callVisionWithFallback({ prompt, frames: input.frames, keys: input.keys });
2036
2203
  return normalizeEditorHarness(attempt.result);
@@ -2437,13 +2604,12 @@ export async function smartDecomposeVideo(input) {
2437
2604
  sceneCuts,
2438
2605
  transcript
2439
2606
  });
2440
- // Phase 2 — vision, OCR, product-placement, and the editor harness run
2441
- // concurrently, all now transcript-aware. OCR and placement failures are
2442
- // non-fatal. The placement + harness calls are DELIBERATELY separate
2443
- // requests from the caption/scene vision call so they can never dilute
2444
- // timeline-layer quality they reuse the same sparse frames (no extra
2445
- // ffmpeg) plus the transcript from phase 1.
2446
- const [attempt, ocrFrames, placementSurfaces, editorHarness] = await Promise.all([
2607
+ // Phase 2 — vision, OCR, and product-placement run concurrently, all now
2608
+ // transcript-aware. OCR and placement failures are non-fatal. The
2609
+ // placement call is DELIBERATELY separate from the caption/scene vision
2610
+ // call so it can never dilute timeline-layer quality it reuses the same
2611
+ // sparse frames (no extra ffmpeg) plus the transcript from phase 1.
2612
+ const [attempt, ocrFrames, placementSurfaces] = await Promise.all([
2447
2613
  callVisionWithFallback({
2448
2614
  prompt,
2449
2615
  frames,
@@ -2461,19 +2627,6 @@ export async function smartDecomposeVideo(input) {
2461
2627
  }).catch((err) => {
2462
2628
  console.error("smart decompose placement pass failed", err instanceof Error ? err.message : err);
2463
2629
  return [];
2464
- }),
2465
- // Editor harness — technical editing direction. Isolated call in the same
2466
- // burst (reuses these frames + the detected cuts + the transcript); non-fatal.
2467
- runEditorHarnessPass({
2468
- frames,
2469
- keys: input.keys,
2470
- durationSeconds: meta.durationSeconds,
2471
- sceneCuts,
2472
- userPrompt: input.userPrompt ?? null,
2473
- transcript
2474
- }).catch((err) => {
2475
- console.error("smart decompose editor-harness pass failed", err instanceof Error ? err.message : err);
2476
- return DEFAULT_EDITOR_HARNESS;
2477
2630
  })
2478
2631
  ]);
2479
2632
  devLog("hyperframes.smart_decompose.ocr_completed", {
@@ -2559,6 +2712,21 @@ export async function smartDecomposeVideo(input) {
2559
2712
  if (input.trendTaglineHint && input.trendTaglineHint.trim().length > 0) {
2560
2713
  viralDna.trend_tagline = input.trendTaglineHint.trim();
2561
2714
  }
2715
+ // Editor harness — technical editing direction. Run AFTER viral DNA is
2716
+ // normalized so the harness can operationalize the exact emotional punch
2717
+ // instead of guessing independently and drifting from the decompose result.
2718
+ const editorHarness = await runEditorHarnessPass({
2719
+ frames,
2720
+ keys: input.keys,
2721
+ durationSeconds: meta.durationSeconds,
2722
+ sceneCuts,
2723
+ userPrompt: input.userPrompt ?? null,
2724
+ transcript,
2725
+ viralDna
2726
+ }).catch((err) => {
2727
+ console.error("smart decompose editor-harness pass failed", err instanceof Error ? err.message : err);
2728
+ return DEFAULT_EDITOR_HARNESS;
2729
+ });
2562
2730
  // NOTE: the per-scene clip-annotation + content-format pass is deliberately
2563
2731
  // NOT run here — it is a separate vision call that would risk the ~60s origin
2564
2732
  // timeout on longer videos. /auto-decompose seeds scene-annotations.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mevdragon/vidfarm-devcli",
3
- "version": "0.20.4",
3
+ "version": "0.20.6",
4
4
  "description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  "scripts": {
33
33
  "dev": "tsx --import ./src/instrument.ts watch src/index.ts",
34
34
  "dev:frontend": "node scripts/build-homepage-client.mjs --watch",
35
- "dev:cli": "tsx src/cli.ts --help",
35
+ "dev:cli": "tsx src/cli.ts",
36
36
  "build:frontend": "node scripts/build-homepage-client.mjs",
37
37
  "build:hyperframes-editor": "cd demo && node scripts/build.mjs",
38
38
  "operator:template-artifact": "node scripts/platform-operator-template-artifact.mjs",