@impractical-ai/motion 0.1.7 → 0.1.10

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.
@@ -41,6 +41,75 @@ export type TransitionResult = {
41
41
  root: HTMLElement
42
42
  }
43
43
 
44
+ /** What the camera dives through. Every mode computes its own geometry —
45
+ * never hand-tune margins or wrap glyphs in spans to fake these. */
46
+ export type ThroughVia =
47
+ | 'surface' // the element's face: its rect covers the frame (default)
48
+ | 'glyph' // one character's COUNTER (the enclosed hole) covers the frame
49
+ | 'word' // one word's rect covers the frame
50
+ | 'anchor'
51
+
52
+ export type ThroughOpts = {
53
+ /** Dive mode. Default 'surface'. 'glyph' needs `char` (or `charIndex`),
54
+ * 'word' needs `word`; both address plain text — no wrapper spans.
55
+ * 'anchor' dives toward the element and, at the cut, match-frames the
56
+ * element matching the SAME selector in the post-swap world (its twin),
57
+ * so the subject persists while everything around it changes. */
58
+ via?: ThroughVia
59
+ /** glyph mode: the character to dive through — first occurrence in the
60
+ * target's text. The dive passes through its enclosed counter (o, e, a,
61
+ * 0…); characters without a hole throw with a diagnostic. */
62
+ char?: string
63
+ /** glyph mode: 0-based index into the target's text, instead of `char`. */
64
+ charIndex?: number
65
+ /** word mode: the word to dive through — first occurrence. */
66
+ word?: string
67
+ /** Takeover color. Defaults: surface/anchor — the target's effective
68
+ * background (first non-transparent backgroundColor walking up from the
69
+ * target); glyph/word — the surface BEHIND the text (the walk starts at
70
+ * the text element's parent), i.e. what actually shows through the
71
+ * counters. */
72
+ color?: string
73
+ /** Dive duration in seconds. Default 0.5. */
74
+ duration?: number
75
+ /** Dive ease. Default 'power3.in' (accelerates into the surface). */
76
+ ease?: string
77
+ /** Cover overshoot: 1.0 = the dive geometry (element rect, glyph counter,
78
+ * or word rect per `via`) exactly covers the frame at the cut. Default
79
+ * 1.15, so a few px of drift can never expose an edge. Override only for
80
+ * taste — every mode measures its own geometry, so no mode NEEDS a
81
+ * hand-tuned margin. */
82
+ margin?: number
83
+ /** The world swap, applied as seek-safe `tl.set`s at the exact cut moment
84
+ * while the frame is fully covered: `hide` gets autoAlpha 0, `show` gets
85
+ * autoAlpha 1. For anything richer, place your own `tl.set`s at the
86
+ * returned `cutAt` time. */
87
+ swap?: { hide?: gsap.TweenTarget; show?: gsap.TweenTarget }
88
+ /** Element in the post-swap world to ARRIVE from: at the cut the camera
89
+ * snaps to this element's own cover framing (measured after the swap), so
90
+ * releasing the overlay reveals it filling the frame, and the pull-out
91
+ * starts inside the new world — the match-frame landing. Omit to pull out
92
+ * from the dive's own framing. */
93
+ land?: string | Element
94
+ /** Framing the pull-out settles on. Default the full frame
95
+ * `{ scale: 1, x: 0, y: 0 }`. */
96
+ settle?: { scale: number; x: number; y: number }
97
+ /** Pull-out duration. Default 1.0. */
98
+ outDuration?: number
99
+ /** Pull-out ease. Default 'expo.out'. */
100
+ outEase?: string
101
+ }
102
+
103
+ export type ThroughResult = {
104
+ /** Full span: dive + pull-out. */
105
+ duration: number
106
+ /** Absolute timeline time of the cut — the frame is guaranteed fully
107
+ * covered here; schedule extra world-swap `tl.set`s at this time. */
108
+ cutAt: number
109
+ /** The takeover overlay's root element. */
110
+ root: HTMLElement
111
+ }
112
+
44
113
  export type TransitionKit = {
45
114
  /** Cover the frame — put this at the end of a clip. Appends to the
46
115
  * timeline's end when no position is given. */
@@ -56,6 +125,29 @@ export type TransitionKit = {
56
125
  opts?: TransitionOpts,
57
126
  position?: gsap.Position
58
127
  ) => TransitionResult
128
+ /** The go-through: dive the camera INTO something until its color takes
129
+ * over the whole frame, cut worlds under the cover, then pull out into the
130
+ * new world. NEVER hand-roll this (a magic-scale zoom + a hand-timed
131
+ * `tl.set` swap + a re-aim) — through() computes the exact cover scale for
132
+ * its dive geometry, measures it in its state AT THE DIVE'S START TIME
133
+ * (entrances, moves, and tilts since build are all accounted for), and
134
+ * snaps a full-frame overlay in the right color at the cut so the
135
+ * takeover is guaranteed to the pixel. Four dive modes via `opts.via`:
136
+ * 'surface' (default — the element's face), 'glyph' (through one
137
+ * character's enclosed counter: `{ via: 'glyph', char: 'o' }` on plain
138
+ * text, no wrapper span — the hole is measured from the actual glyph
139
+ * outline, so the margin is computed, never tuned), 'word' (`{ via:
140
+ * 'word', word: 'world' }` — the word's rect), and 'anchor' (match-cut:
141
+ * dive toward the element, swap the world at the cut, and land on the
142
+ * element matching the same selector in the new world, so the subject
143
+ * persists while everything around it changes). Deterministic: every
144
+ * value is resolved once at build; the swap is `tl.set`s, never
145
+ * callbacks. Appends at the timeline's end when no position is given. */
146
+ through: (
147
+ target: string | Element,
148
+ opts?: ThroughOpts,
149
+ position?: gsap.Position
150
+ ) => ThroughResult
59
151
  }
60
152
 
61
153
  // ─── Text — named, deterministic text animations ─────────────────────────
@@ -1050,11 +1142,16 @@ export type SentenceHandle = {
1050
1142
  // ─── Morph — any element becomes any other element ───────────────────────
1051
1143
 
1052
1144
  export type MorphOpts = {
1053
- /** 'apple' = expo-out flight (~90% of the change in the first ~15% of the
1054
- * time, long soft landing). 'spring' = the reference bounce: expo-out
1055
- * flight, then a LIGHT damped sine rides the same path — calibrated from
1056
- * the dnyxstudios AE expression (amp .04·v, freq 1.8Hz, decay e^-3t,
1057
- * eased envelope). One motion, no delayed settle, ~one visible overshoot. */
1145
+ /** 'apple' = a firm in-out flight that keeps the deformation READABLE —
1146
+ * the shape change is the subject, so it may not be over in three frames.
1147
+ * 'spring' = the same flight with the reference bounce: a LIGHT damped
1148
+ * sine riding the same path, calibrated from the dnyxstudios AE expression
1149
+ * (amp .04·v, freq 1.8Hz, decay e^-3t, eased envelope). One motion, no
1150
+ * delayed settle, ~one visible overshoot.
1151
+ *
1152
+ * Both are `ease-in-out-*` by design. A morph is travel WITHIN the frame,
1153
+ * and the house table reserves `ease-out-*` for entrances; a front-loaded
1154
+ * curve finishes the transformation before the viewer can look at it. */
1058
1155
  feel?: 'apple' | 'spring'
1059
1156
  /** Tune the 'spring' bounce. `amp` = first overshoot as a fraction of the
1060
1157
  * travel (default 0.025 — light), `freq` Hz (default 1.8), `decay` per
@@ -1065,17 +1162,26 @@ export type MorphOpts = {
1065
1162
  /** Flight curve — ANY GSAP ease works: house names, 'elastic.out(1, 0.4)',
1066
1163
  * 'back.out(1.8)'… Overshoots extrapolate past the target (kept renderable). */
1067
1164
  ease?: string
1068
- /** 0..1 — center of the A→B crossfade along the flight. */
1165
+ /** 0..1 in EASED space — where along the SHAPE CHANGE the surface swaps
1166
+ * from A to B. Not a fade midpoint: at this instant both sides hold the
1167
+ * same box and the same chrome, so the swap is a match-frame cut. 0.5
1168
+ * means "when the shape is halfway between the two," which is what the
1169
+ * viewer actually perceives. */
1069
1170
  handoff?: number
1070
- /** Width of the crossfade as a fraction of the flight (default 0.3 —
1071
- * tight; each side shows only near its natural shape). */
1072
- crossfade?: number
1171
+ /** Fraction of the flight over which unmatched content departs and
1172
+ * arrives (default 0.42). Content displaces along the travel vector and
1173
+ * must be clear of the seam — that emptiness is what makes the cut
1174
+ * invisible, so widening this past ~0.5 will start to show the swap. */
1175
+ content?: number
1073
1176
  /** Arc lift in px (0 disables). Default: auto from travel distance. */
1074
1177
  arc?: number
1075
- /** Part choreography (default 'stagger'): the target's direct children
1076
- * arrive individually inside the landing box; the source's children are
1077
- * absorbed as it leaves. `false` = containers only. */
1078
- parts?: 'stagger' | false
1178
+ /** Content choreography (default 'displace'): unmatched children leave and
1179
+ * arrive by moving ALONG the travel vector — replacement reads as motion,
1180
+ * and both sides are clear of the seam before the surfaces swap.
1181
+ * `false` = containers only; children hold still and stay counter-scaled,
1182
+ * which is right when the two sides share a background image or the
1183
+ * content is meant to look continuous. */
1184
+ parts?: 'displace' | false
1079
1185
  /** Sub-component matching (default 'auto'): elements tagged
1080
1186
  * `data-morph="name"` on BOTH sides (any depth) morph INTO each other —
1081
1187
  * each pair rides its own interpolated box inside the flight, so the
@@ -1368,9 +1474,12 @@ export type DirectionContext = {
1368
1474
  * a long dwell at both ends and a confident glide through the middle.
1369
1475
  * These defaults ARE the recommended camera physics: do NOT pass ease
1370
1476
  * or duration here unless the user explicitly asks for a different
1371
- * camera feel. The two sanctioned exceptions: a slow ambient drift
1372
- * (`duration: 2–5, ease: 'none'`) and an instant reframe behind a
1373
- * covering element (`duration: 0.01`). Anything between 0.05–0.4s is
1477
+ * camera feel. The one sanctioned exception: an instant reframe behind
1478
+ * a covering element (`duration: 0.01`). A slow linear drift
1479
+ * (`duration: 2–5, ease: 'none'`) is NOT one — sub-pixel motion on the
1480
+ * whole stage re-rasterizes every glyph each frame and renders as
1481
+ * micro-flicker; the motion lab flags it as DRIFT and export blocks.
1482
+ * Hold the camera still instead. Anything between 0.05–0.4s is
1374
1483
  * neither a move nor a cut — avoid it. `ease-in-out-cubic` is for
1375
1484
  * ELEMENT travel, never the camera. */
1376
1485
  move: (
@@ -1427,7 +1536,21 @@ export type DirectionContext = {
1427
1536
  * 'pixels', 'iris', opts `{ color?, direction?: 'left'|'right'|'up'|
1428
1537
  * 'down', duration?, cell?, at? }`. `out` with no position appends at the
1429
1538
  * clip's end — the standard way to end a clip for a cut; `in` at position
1430
- * 0 starts the clip covered and reveals it. All return `{ duration }` (full span including
1539
+ * 0 starts the clip covered and reveals it.
1540
+ * `transition.through(target, { via?, char?, word?, swap?, land?, color?,
1541
+ * duration?, outDuration? }, position?)` is the GO-THROUGH: dive INTO
1542
+ * something until its color takes over the frame, world-swap under the
1543
+ * cover, pull out into the new world. `via` picks the dive geometry:
1544
+ * 'surface' (default, the element's face), 'glyph' (`{ via: 'glyph',
1545
+ * char: 'o' }` — through the character's enclosed counter, measured from
1546
+ * the real glyph outline on plain text: no wrapper spans, no hand-tuned
1547
+ * margins), 'word' (`{ via: 'word', word: 'world' }` — the word's rect),
1548
+ * 'anchor' (match-cut: the subject persists across the cut — the same
1549
+ * selector's twin in the new world is the landing — while everything
1550
+ * around it changes). Never hand-roll a dive (a magic-scale camera.move +
1551
+ * a hand-timed swap): through() computes the exact cover scale, measures
1552
+ * at the dive's own start time, and guarantees full coverage at the cut.
1553
+ * All return `{ duration }` (full span including
1431
1554
  * stagger) for sequencing, and accept `(target, effect, opts?, position?)`
1432
1555
  * with opts `{ by?: 'chars'|'words'|'lines', duration?, stagger?, ease?,
1433
1556
  * from?: 'start'|'center'|'end'|'edges' }`. */
@@ -1596,10 +1719,17 @@ export type DirectionContext = {
1596
1719
  * eases the container instead of live reflow. */
1597
1720
  sentence: SentenceKit['sentence']
1598
1721
  /** Universal shape morph: any element becomes any other element, a rect, or
1599
- * `'frame'`. FLIP box + interpolated rounded-rect clip-path silhouette +
1600
- * content-clone crossfade (`handoff` sets where). Author the TARGET hidden
1601
- * (`autoAlpha: 0`); the morph reveals it at arrival. `fromPath`/`toPath`
1602
- * accept real SVG paths for logo endcards. Export-worker safe. */
1722
+ * `'frame'`. Both real elements ride ONE interpolated box (position, size,
1723
+ * corner radius, arc lift) while the source's chrome is driven onto the
1724
+ * target's — at `handoff` the two are the same picture, so the surfaces
1725
+ * HARD-CUT and never crossfade. Matched sub-components (`data-morph="name"`
1726
+ * on both sides) travel; unmatched content displaces along the travel
1727
+ * vector; children are counter-scaled so nothing stretches.
1728
+ *
1729
+ * Author the TARGET hidden (`autoAlpha: 0`); the morph reveals it at
1730
+ * arrival. Every visual state is a pure function of eased progress, so it
1731
+ * is export-worker safe. For MANY-to-many structure — recursing into
1732
+ * sub-components, N→1 gathers, 1→N splits — install `kit/morphtree`. */
1603
1733
  morph: MorphKit['morph']
1604
1734
  /** The chip⇄full-bleed round trip: grows the element to cover the frame,
1605
1735
  * holds, returns home (`back: false` to stay). Scale IS the transition —
@@ -1700,8 +1830,9 @@ export type BuiltScene = {
1700
1830
  // promo-in ease-in-quart (.895,.03,.685,.22) — exits, dives
1701
1831
  //
1702
1832
  // Rule of thumb from the reference promos: entrances ~0.4–0.6s on promo-out
1703
- // (visually settled inside 0.15s), line staggers ~0.2s, holds carry a slow
1704
- // linear scale drift (~4%/s, ease 'none'), pops that need bounce use
1833
+ // (visually settled inside 0.15s), line staggers ~0.2s, holds are held STILL
1834
+ // (a slow linear scale drift re-rasterizes glyphs every frame and renders as
1835
+ // micro-flicker — never add one), pops that need bounce use
1705
1836
  // back.out(1.8–2), and background/color cuts are HARD (no crossfade).
1706
1837
 
1707
1838
  /** Registered ease names usable anywhere GSAP accepts an ease. */
@@ -10,7 +10,7 @@ build, the render, and the studio the user tweaks in — you provide the
10
10
  direction. Everything runs through the **Impractical MCP server**; the video's
11
11
  source code lives in the cloud, never in the user's project directory.
12
12
 
13
- ## 0. Setup check (once per session)
13
+ ## Setup check (once per session)
14
14
 
15
15
  Call the `account_status` tool (`whoami` is its legacy alias). If the
16
16
  Impractical tools are not available, the current coding agent is not connected.
@@ -19,269 +19,41 @@ Tell the user to run:
19
19
  npx --yes @impractical-ai/motion setup --agent
20
20
 
21
21
  Then ask them to restart or reload their coding agent and type `continue`.
22
- Claude Code users who specifically want the full plugin can run the same command
23
- with `--claude` instead. If the weekly quota is exhausted, call `upgrade` to give
24
- the user the authenticated billing page.
25
-
26
- ## 1. Read the engine before you write anything
27
-
28
- Call **`engine_types`** first. It returns the TypeScript surface a scene is
29
- written against: the `ctx` object, every named text/logo/transition effect with
30
- its options, the named eases, and the determinism rules. Writing a direction
31
- without reading it produces slop.
32
-
33
- ## 2. Make a video
34
-
35
- Before scaffolding, call **`motion_context`** with the user's brief — plus
36
- `brandSlug` and `contentType` when you know them. It returns the current
37
- versioned house skills, the best matching production references, available
38
- template schemas, and rendered reference frames. Study those frames and sources
39
- before choosing a template. The library is live: do not rely on a remembered
40
- catalog from an earlier session.
41
-
42
- When the workspace has personalization enabled, the result also carries the
43
- user's resolved motion profile: `personalStyleSkill` (confirmed preferences —
44
- follow it like house guidance), `acceptedExamples`, a reranked
45
- style/template order, and a `contextId`. Pass that `contextId` to
46
- `create_video` as `motionContextId` so the draft is pinned to the
47
- recommendations that produced it.
48
-
49
- Gather from the user (ask only for what's missing):
50
-
51
- - **Value prop** (required): one or two sentences on what ships and why it matters.
52
- - Optional: their product's front-end code, a brand URL, screen recordings.
53
-
54
- Before `create_video`, write a compact creative contract. This is a planning
55
- record, not a new scene format:
56
-
57
- - audience
58
- - the single idea the viewer should remember
59
- - the visual moment that proves it without explanatory copy
60
- - how the reference's hierarchy/pacing will be translated rather than copied
61
- - the real product source (and whether anything must be reconstructed)
62
- - opening, proof, and payoff
63
-
64
- When the local quality-loop experiment is enabled, pass that contract as
65
- `creativeContract` to `create_video`. The tool will reject an unplanned start.
66
-
67
- If they gave a brand URL and no kit exists yet:
68
-
69
- brand_list
70
- brand_extract { url: "https://example.com" }
71
- brand_extract { jobId: "<returned run id>", wait: true }
72
-
73
- Then scaffold the project:
74
-
75
- create_video { valueProp: "...", brandSlug?: "example-com", appCode?: "...", templateId?: "...", templateParams?: {...} }
76
-
77
- This creates the project and a draft with starter files. It deliberately does
78
- **not** run a server-side director — you are the director.
79
-
80
- ## 3. Direct it
81
-
82
- A scene is two files, and both must exist for the preview to build:
83
-
84
- - **`App.tsx`** — the stage markup. Plain React and CSS; this is the product UI
85
- on screen.
86
- - **`direction.ts`** — the choreography. `export default (ctx) => { ... }`
87
- against one paused GSAP timeline.
88
-
89
- Write them with `write_draft_files` (whole files) or `edit_draft_file` (exact
90
- string replacement — prefer this for revisions). Every save rebuilds the
91
- preview and returns the committed revision plus its build job id immediately.
92
- Follow that id with `job_status { jobIds: ["..."], wait: true }`; that result is
93
- your compile loop, so read and fix any build error rather than guessing.
94
-
95
- Rules that matter:
96
-
97
- - `brand.json` is law for colors and fonts. Read it; do not invent a palette.
98
- - Never edit `project.json`.
99
- - The timeline is rendered by seeking frame by frame. Anything that is not a
100
- pure function of timeline position — `Date.now()`, `Math.random()`,
101
- `setTimeout`, CSS animations, autoplaying `<video>` — desynchronises the
102
- export. Use `ctx.video()` for footage.
103
- - Only `.ts/.tsx/.js/.jsx/.css/.json` files sync, max 100 per draft.
104
-
105
- Craft, briefly — the full language is `guidance.motionDirection` in the
106
- `motion_context` result. That copy is live and versioned; this summary is a
107
- snapshot, so when the two disagree, the tool result wins.
108
-
109
- - **Beat it out before writing code.** Every beat needs a time, a visible
110
- subject, and a narrative purpose. An intentional reading hold is valid.
111
- - **Text enters on `rise`** — `text.reveal(el, 'rise')` on `promo-out`, lines
112
- ~0.2s apart. Do *not* default type to `'blur'`: word-by-word de-blur reads as
113
- out-of-focus, and across a stagger it turns to mush. Blur is for whole-frame
114
- moves — `fx.spike(el, 'blur')` alongside a whip or dive, where it hides the
115
- cut. Never animate blur above 20px.
116
- - **Every ease is a named house curve.** `ease-out-*` for entrances,
117
- `ease-in-out-*` for travel and every camera move, `ease-in-*` for exits and
118
- dives only. Never leave an ease unspecified; never put `ease-in-*` on an
119
- entrance. The promo subset: `promo-out`, `promo-soft`, `promo-inout`,
120
- `promo-in`, `launch-out`.
121
- - **Decisive**: entrances 0.4–0.6s, exits shorter, staggers ~0.2s apart.
122
- Longer movement needs a narrative reason; important states may hold still.
123
- - Budget one primary and at most one supporting motion. Keep backgrounds stable
124
- unless their movement is explicitly authored.
125
- - Hold long enough to read. A beat that looks legible scrubbing the timeline is
126
- usually still too fast on screen.
127
- - Cut hard between sections; crossfading backgrounds reads as cheap.
128
- - One idea per beat. If two things move for different reasons at once, split them.
129
-
130
- ### The five moves that separate a real video from a generated one
131
-
132
- These are the engine's `ctx.fx` and `ctx.mask` kits. Reaching for them is most
133
- of the gap between output that looks authored and output that looks scripted.
134
- Full signatures are in the engine types; the rules of thumb are:
135
-
136
- 1. **Hide every whip cut with one spike, never two tweens.**
137
- ```ts
138
- camera.move(camera.framing('[data-next]'), { duration: 0.28, ease: 'promo-inout' }, 'whip')
139
- fx.spike('[data-panel]', 'blur', { amount: 14 }, 'whip') // ONE call
140
- ```
141
- Hand-writing `tl.to(stage, { filter: 'blur(12px)' })` up and back is the
142
- single most common way this looks amateur: the two tweens drift out of sync
143
- and the user gets two unlinked controls. `fx.spike` is one tween, in and
144
- out, locked to the move.
145
-
146
- 2. **Feather full-frame reveals.** `mask.reveal(el, 'feather', { direction, softness: 0.3 })`
147
- — a hard edge across a whole frame reads cheap, and `clip-path` cannot make
148
- a soft one at all. Use `wipe`/`iris`/`slit`/`barn` when you want the hard
149
- edge deliberately; `radial` and `blinds` for softer variety.
150
-
151
- 3. **Land on light, not on opacity.** One `fx.spike(el, 'bloom')` as a beat
152
- settles reads as a light source. Fading opacity reads as a slideshow.
153
-
154
- 4. **Put colour inside the letters.** `mask.fill(el, { gradient, drift })` uses
155
- `background-clip: text`, so the gradient lives in the glyphs and moves
156
- through them. Far stronger than a coloured headline, and one call.
157
-
158
- 5. **Composite light instead of shipping a PNG.**
159
- `mask.blend(disc, 'screen')` behind type gives glow with no asset.
160
- `multiply` for ink on cream, `luminosity` for duotone.
161
-
162
- Two things to get right or the effect misfires:
163
-
164
- - **Grain must be continuous.** Apply it once to a full-frame plate that sits
165
- above every beat and is never cut (`fx.apply(plate, 'grain', { amount: 0.2 })`),
166
- or it visibly blinks at each hard cut.
167
- - **Effects are seasoning.** One or two per beat. `displace`, `chroma`, and
168
- `threshold` are accents for a single hit, not a look to hold.
169
-
170
- Ask `motion_context` for the **`optical`** style to see all of this composed in
171
- one reference, or the **`whip-cut`**, **`soft-reveal`**, and **`gradient-type`**
172
- templates for a working scene you can adapt.
173
-
174
- ## 4. Look at your work
175
-
176
- get_frames { draftId: "...", times: [0, 1, 2, 3] }
177
-
178
- This renders real frames on the user's machine and returns them as images. Ask
179
- for several times in one call — once the page is open the extra frames are
180
- nearly free.
181
-
182
- **Always look at frames before telling the user a change is done.** If a frame
183
- shows text clipped, an element off-frame, or a blank region, fix it and look
184
- again. This is the difference between shipping a video and shipping a guess.
185
-
186
- When the `get_motion` tool is available (the local motion lab experiment),
187
- prefer it as the first look after every preview build:
188
-
189
- get_motion { draftId: "..." } // whole scene
190
- get_motion { draftId: "...", from: 4.8, to: 6.4 } // one weak window
191
-
192
- Static frames measure composition; `get_motion` measures motion — dense
193
- contact sheets where the difference between adjacent tiles IS the motion,
194
- plus measured pixel change per interval, dead-air spans, and the timeline's
195
- actual tweens with any that run without producing visible change. Fix what it
196
- flags, then use `get_frames` for composition close-ups.
197
-
198
- With the quality loop also enabled, the final review is two-part and export
199
- requires both on the same revision: `get_frames { reviewMode: "final" }` for
200
- composition and legibility at native resolution, and
201
- `get_motion { draftId, reviewMode: "final" }` for full-duration motion
202
- coverage at one frame in five.
203
-
204
- For a new launch video, build and review the proof beat before completing the
205
- rest. When the local quality loop is enabled:
206
-
207
- get_frames { draftId: "...", times: [/* proof beat */], reviewMode: "proof" }
208
-
209
- For the final pass, review at least eight representative timestamps including
210
- `t=0` and the settled closing frame:
211
-
212
- get_frames { draftId: "...", times: [/* whole timeline */], reviewMode: "final" }
213
-
214
- The final critic scores the visible result—not the code or effort—on narrative
215
- clarity, visual proof, product authenticity, composition, legibility, pacing,
216
- reference fidelity, and technical integrity. Use timestamped evidence. A weak
217
- score is a revision brief, not permission to describe the cut as “clean.”
218
-
219
- ## 5. Export and hand off
220
-
221
- export_video { draftId: "...", qualityReview?: {/* required by local quality loop */} }
222
- job_status { jobIds: ["<returned job id>"], wait: true }
223
- studio_link { projectId: "...", slug: "..." }
224
-
225
- Always give the user the studio link — that is where they watch the cuts and
226
- tweak visually. The export URL expires, so only export once they are happy.
22
+ Claude Code users who specifically want the full plugin can run the same
23
+ command with `--claude` instead. If the weekly quota is exhausted, call
24
+ `upgrade` to give the user the authenticated billing page.
25
+
26
+ ## Get the playbook, then follow it
27
+
28
+ The full, current director playbook is SERVED, not bundled: call **`motion_context`**
29
+ with the user's brief (plus `brandSlug` and `contentType`
30
+ when known). Its `guidance.director` field carries the complete house playbook
31
+ for this library version — workflow, review discipline, export rules. Read it
32
+ and follow it before writing any scene code. `engine_types` returns the
33
+ TypeScript contract a scene is written against; read it before writing a
34
+ direction.
35
+
36
+ ## The tool loop (the shape; the served playbook has the craft)
37
+
38
+ Create with `create_video`, then write `App.tsx` and `direction.ts` with
39
+ `write_draft_files` (whole files) or `edit_draft_file` (exact string
40
+ replacement — prefer it for revisions). Every save rebuilds the preview and
41
+ returns the committed revision plus its build job id immediately; follow that
42
+ id with `job_status` and fix any build error it returns. Call `install_kits`
43
+ once right after `create_video` — the kit sources are served into the draft.
44
+ `brand_extract` builds a brand kit from a URL. Call `get_frames` to SEE the
45
+ result before telling the user it is done, then `export_video`, then hand the
46
+ user the `studio_link`.
227
47
 
228
48
  ## Timeouts and lost acknowledgements
229
49
 
230
50
  Build, export, and brand-extraction calls return their job or run id before
231
- waiting. Continue build/export ids with `job_status`, and continue a brand run
232
- with `brand_extract { jobId, wait: true }` (never the URL again). If the MCP
233
- client kills any call and shows only a timeout, the mutation may still have
234
- committed even though its acknowledgement was lost.
235
- **Never blindly retry a mutating or metered call.** Verify state first:
236
-
237
- - After `write_draft_files` / `edit_draft_file`, use `list_draft_files` and
238
- `read_draft_file` to confirm the intended revision and source.
239
- - After an export or other project mutation, use `get_project` / `list_projects`
240
- to check whether it already started or completed.
241
- - After `brand_extract`, continue its returned id with `brand_extract { jobId, wait: true }`. If the
242
- start acknowledgement was lost, check `brand_list` / `brand_get` before
243
- starting another extraction, especially when `reextract: true`.
244
-
245
- Retry only when those reads prove the mutation did not commit. Otherwise
246
- continue from the state you observed; a duplicate edit or export wastes quota.
247
-
248
- ## The personal motion profile
249
-
250
- The `motion_profile` tool manages what Impractical remembers about this user's
251
- taste. The rules are strict because trust in the profile depends on them:
252
-
253
- - **Read before directing.** The personalized context from `motion_context`
254
- already includes the resolved profile; call `motion_profile { action: 'get' }`
255
- only when the user asks about their profile or you need rule keys.
256
- - **`remember` is for explicit requests only** — "remember that I like…",
257
- "always keep my videos under 15 seconds". Never call `remember` for a
258
- preference you merely inferred.
259
- - **Inferred preferences go through `propose`.** If the user's feedback during
260
- this session implies a durable preference, submit it as a structured rule
261
- with `propose`. It becomes a pending proposal that has zero effect until the
262
- user confirms it.
263
- - **Present at most one pending proposal, and only after a completed video.**
264
- If `motion_context` returned `pendingProposal`, mention it once after the
265
- export or when the user is clearly done — one sentence, e.g. "Want me to
266
- remember that you prefer faster pacing? (yes/no)". Call `confirm` or
267
- `reject` with their answer. Never present more than one, never mid-work, and
268
- **never silently confirm**.
269
- - The user can `pause`, `forget` a single rule, or `reset` everything; do these
270
- only when asked.
271
-
272
- ## Conflicts
273
-
274
- The web tweak chat edits the same draft. `edit_draft_file` reads, edits and
275
- writes in one call against the current revision, so ordinary races resolve
276
- themselves. If a tool reports the draft changed underneath your edit, re-read
277
- the file and re-apply your change on top of the new content — never paste stale
278
- content back.
279
-
280
- If a tool reports the draft is locked by a job, wait for that job with
281
- `job_status { wait: true }` and retry.
282
-
283
- ## Costs
284
-
285
- - Preview builds and exports run on Impractical's infrastructure; the direction
286
- thinking is yours, so iterate on the code freely.
287
- - Preview rebuilds take tens of seconds. Batch your edits, then look at frames.
51
+ waiting. If the MCP client kills a call and shows only a timeout, the mutation
52
+ may still have committed. **Never blindly retry a mutating or metered call.**
53
+ Verify state first: after a write, use `list_draft_files` and
54
+ `read_draft_file` to confirm the intended revision; after an export, check
55
+ `get_project`; after `brand_extract`, continue its returned id.
56
+ Retry only when those reads prove the mutation did not commit.
57
+
58
+ Before directing an existing draft, read `notes.json` — the user's queued
59
+ edits come first; mark each handled item `done`.
@@ -13,8 +13,8 @@ import {
13
13
  waitForJobs,
14
14
  withAdmissionReceipt,
15
15
  withMeteredCall
16
- } from "./chunk-MQKW7NFI.js";
17
- import "./chunk-Z74SLKEU.js";
16
+ } from "./chunk-CZANULCU.js";
17
+ import "./chunk-6FVZ6IWR.js";
18
18
  import "./chunk-77HVPD4G.js";
19
19
  export {
20
20
  ApiContractError,
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@impractical-ai/motion",
6
- version: "0.1.7",
6
+ version: "0.1.10",
7
7
  private: false,
8
8
  description: "Create and refine Impractical Motion videos from coding agents and the terminal.",
9
9
  homepage: "https://github.com/Openpod/impractical-motion#readme",
@@ -28,9 +28,9 @@ var package_default = {
28
28
  node: ">=22"
29
29
  },
30
30
  scripts: {
31
- build: 'node ../scripts/verify-locked-cli-install.mjs && node ../scripts/build-engine-dts.mjs --cli-only && rm -rf dist && ./node_modules/esbuild/bin/esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --splitting --outdir=dist --external:playwright-core --banner:js="#!/usr/bin/env node" && chmod +x dist/index.js',
31
+ build: 'node ../scripts/verify-locked-cli-install.mjs && node ../scripts/build-engine-dts.mjs --cli-only && rm -rf dist && ./node_modules/esbuild/bin/esbuild src/index.ts --define:__BUILD_COMMIT__=\\"$(git rev-parse --short HEAD 2>/dev/null || echo dev)\\" --bundle --platform=node --format=esm --target=node22 --splitting --outdir=dist --external:playwright-core --banner:js="#!/usr/bin/env node" && chmod +x dist/index.js',
32
32
  typecheck: "tsc --noEmit",
33
- test: "npm run typecheck && npm run build && esbuild tests/onboarding.test.ts tests/mcp-tools.test.ts tests/mcp-frames.test.ts tests/mcp-motion.test.ts tests/mcp-motion-score.test.ts tests/mcp-install-font.test.ts tests/mcp-metric-catalog.test.ts tests/mcp-text-census.test.ts tests/mcp-phased-flow.test.ts tests/mcp-preflight.test.ts tests/engine-dts.test.ts tests/mcp-connect.test.ts tests/mcp-longpoll.test.ts tests/mcp-agent-timeout.test.ts tests/api-hardening.test.ts --bundle --platform=node --format=esm --packages=external --outdir=dist --out-extension:.js=.mjs && node --test dist/onboarding.test.mjs dist/mcp-tools.test.mjs dist/mcp-frames.test.mjs dist/mcp-motion.test.mjs dist/mcp-motion-score.test.mjs dist/mcp-install-font.test.mjs dist/mcp-metric-catalog.test.mjs dist/mcp-text-census.test.mjs dist/mcp-phased-flow.test.mjs dist/mcp-preflight.test.mjs dist/engine-dts.test.mjs dist/mcp-connect.test.mjs dist/mcp-longpoll.test.mjs dist/mcp-agent-timeout.test.mjs dist/api-hardening.test.mjs",
33
+ test: "npm run typecheck && npm run build && esbuild tests/onboarding.test.ts tests/mcp-tools.test.ts tests/mcp-frames.test.ts tests/mcp-browser-pool.test.ts tests/mcp-motion.test.ts tests/mcp-craft.test.ts tests/mcp-depthkit.test.ts tests/mcp-motion-score.test.ts tests/mcp-install-font.test.ts tests/mcp-metric-catalog.test.ts tests/mcp-text-census.test.ts tests/mcp-phased-flow.test.ts tests/mcp-preflight.test.ts tests/engine-dts.test.ts tests/mcp-connect.test.ts tests/mcp-longpoll.test.ts tests/mcp-agent-timeout.test.ts tests/api-hardening.test.ts --bundle --platform=node --format=esm --packages=external --outdir=dist --out-extension:.js=.mjs && node --test dist/onboarding.test.mjs dist/mcp-tools.test.mjs dist/mcp-frames.test.mjs dist/mcp-browser-pool.test.mjs dist/mcp-motion.test.mjs dist/mcp-craft.test.mjs dist/mcp-depthkit.test.mjs dist/mcp-motion-score.test.mjs dist/mcp-install-font.test.mjs dist/mcp-metric-catalog.test.mjs dist/mcp-text-census.test.mjs dist/mcp-phased-flow.test.mjs dist/mcp-preflight.test.mjs dist/engine-dts.test.mjs dist/mcp-connect.test.mjs dist/mcp-longpoll.test.mjs dist/mcp-agent-timeout.test.mjs dist/api-hardening.test.mjs",
34
34
  prepack: "npm run build"
35
35
  },
36
36
  publishConfig: {
@@ -56,6 +56,8 @@ var PRODUCTION_APP_URL = "https://motion.impractical.ai";
56
56
  var PRODUCTION_SUPABASE_URL = "https://uaxjweednprtbhbceify.supabase.co";
57
57
  var PACKAGE_NAME = "@impractical-ai/motion";
58
58
  var PACKAGE_VERSION = package_default.version;
59
+ var BUILD_COMMIT = "779568a" ? "779568a" : "dev";
60
+ var BUILD_ID = `${PACKAGE_VERSION}+${BUILD_COMMIT}`;
59
61
  var PACKAGE_COMMAND = `npx --yes ${PACKAGE_NAME}`;
60
62
 
61
63
  export {
@@ -63,5 +65,6 @@ export {
63
65
  PRODUCTION_SUPABASE_URL,
64
66
  PACKAGE_NAME,
65
67
  PACKAGE_VERSION,
68
+ BUILD_COMMIT,
66
69
  PACKAGE_COMMAND
67
70
  };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  PRODUCTION_APP_URL,
4
4
  PRODUCTION_SUPABASE_URL
5
- } from "./chunk-Z74SLKEU.js";
5
+ } from "./chunk-6FVZ6IWR.js";
6
6
 
7
7
  // src/config.ts
8
8
  import {