@impractical-ai/motion 0.1.7 → 0.1.9
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/assets/engine.d.ts +65 -1
- package/assets/impractical/SKILL.md +35 -263
- package/dist/{api-DR3NJ7TI.js → api-NVZQJBVP.js} +2 -2
- package/dist/{chunk-Z74SLKEU.js → chunk-H5TR3QJN.js} +6 -3
- package/dist/{chunk-MQKW7NFI.js → chunk-KI5E2464.js} +1 -1
- package/dist/{chunk-M7SHFM25.js → chunk-NG3Q3BYW.js} +57 -4
- package/dist/{commands-R3LJU2BW.js → commands-56WH3ZVJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/{stdio-XUVQKDCB.js → stdio-MJ4LTXME.js} +2717 -1454
- package/package.json +3 -3
- package/assets/kits/actions.ts +0 -803
- package/assets/kits/ensemble.ts +0 -483
- package/assets/kits/highlight.ts +0 -374
- package/assets/kits/impact.ts +0 -103
- package/assets/kits/kits.ts +0 -343
- package/assets/kits/looks.ts +0 -935
- package/assets/kits/morphkit.ts +0 -438
- package/assets/kits/reveal.ts +0 -1364
- package/assets/kits/sentence.ts +0 -218
- package/assets/kits/streamkit.ts +0 -195
package/assets/engine.d.ts
CHANGED
|
@@ -41,6 +41,48 @@ export type TransitionResult = {
|
|
|
41
41
|
root: HTMLElement
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export type ThroughOpts = {
|
|
45
|
+
/** Takeover color. Default: the target's effective background (first
|
|
46
|
+
* non-transparent backgroundColor walking up from the target), read at
|
|
47
|
+
* build time. */
|
|
48
|
+
color?: string
|
|
49
|
+
/** Dive duration in seconds. Default 0.5. */
|
|
50
|
+
duration?: number
|
|
51
|
+
/** Dive ease. Default 'power3.in' (accelerates into the surface). */
|
|
52
|
+
ease?: string
|
|
53
|
+
/** Cover overshoot: 1.0 = the target exactly covers the frame at the cut.
|
|
54
|
+
* Default 1.15, so a few px of layout drift can never expose an edge. */
|
|
55
|
+
margin?: number
|
|
56
|
+
/** The world swap, applied as seek-safe `tl.set`s at the exact cut moment
|
|
57
|
+
* while the frame is fully covered: `hide` gets autoAlpha 0, `show` gets
|
|
58
|
+
* autoAlpha 1. For anything richer, place your own `tl.set`s at the
|
|
59
|
+
* returned `cutAt` time. */
|
|
60
|
+
swap?: { hide?: gsap.TweenTarget; show?: gsap.TweenTarget }
|
|
61
|
+
/** Element in the post-swap world to ARRIVE from: at the cut the camera
|
|
62
|
+
* snaps to this element's own cover framing (measured after the swap), so
|
|
63
|
+
* releasing the overlay reveals it filling the frame, and the pull-out
|
|
64
|
+
* starts inside the new world — the match-frame landing. Omit to pull out
|
|
65
|
+
* from the dive's own framing. */
|
|
66
|
+
land?: string | Element
|
|
67
|
+
/** Framing the pull-out settles on. Default the full frame
|
|
68
|
+
* `{ scale: 1, x: 0, y: 0 }`. */
|
|
69
|
+
settle?: { scale: number; x: number; y: number }
|
|
70
|
+
/** Pull-out duration. Default 1.0. */
|
|
71
|
+
outDuration?: number
|
|
72
|
+
/** Pull-out ease. Default 'expo.out'. */
|
|
73
|
+
outEase?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ThroughResult = {
|
|
77
|
+
/** Full span: dive + pull-out. */
|
|
78
|
+
duration: number
|
|
79
|
+
/** Absolute timeline time of the cut — the frame is guaranteed fully
|
|
80
|
+
* covered here; schedule extra world-swap `tl.set`s at this time. */
|
|
81
|
+
cutAt: number
|
|
82
|
+
/** The takeover overlay's root element. */
|
|
83
|
+
root: HTMLElement
|
|
84
|
+
}
|
|
85
|
+
|
|
44
86
|
export type TransitionKit = {
|
|
45
87
|
/** Cover the frame — put this at the end of a clip. Appends to the
|
|
46
88
|
* timeline's end when no position is given. */
|
|
@@ -56,6 +98,21 @@ export type TransitionKit = {
|
|
|
56
98
|
opts?: TransitionOpts,
|
|
57
99
|
position?: gsap.Position
|
|
58
100
|
) => TransitionResult
|
|
101
|
+
/** The go-through: dive the camera INTO an element until its color takes
|
|
102
|
+
* over the whole frame, cut worlds under the cover, then pull out into the
|
|
103
|
+
* new world. NEVER hand-roll this (a magic-scale zoom + a hand-timed
|
|
104
|
+
* `tl.set` swap + a re-aim) — through() computes the exact cover scale for
|
|
105
|
+
* the target, measures the target in its state AT THE DIVE'S START TIME
|
|
106
|
+
* (entrances, moves, and tilts since build are all accounted for), and
|
|
107
|
+
* snaps a full-frame overlay in the target's color at the cut so the
|
|
108
|
+
* takeover is guaranteed to the pixel. Deterministic: every value is
|
|
109
|
+
* resolved once at build; the swap is `tl.set`s, never callbacks.
|
|
110
|
+
* Appends at the timeline's end when no position is given. */
|
|
111
|
+
through: (
|
|
112
|
+
target: string | Element,
|
|
113
|
+
opts?: ThroughOpts,
|
|
114
|
+
position?: gsap.Position
|
|
115
|
+
) => ThroughResult
|
|
59
116
|
}
|
|
60
117
|
|
|
61
118
|
// ─── Text — named, deterministic text animations ─────────────────────────
|
|
@@ -1427,7 +1484,14 @@ export type DirectionContext = {
|
|
|
1427
1484
|
* 'pixels', 'iris', opts `{ color?, direction?: 'left'|'right'|'up'|
|
|
1428
1485
|
* 'down', duration?, cell?, at? }`. `out` with no position appends at the
|
|
1429
1486
|
* 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.
|
|
1487
|
+
* 0 starts the clip covered and reveals it.
|
|
1488
|
+
* `transition.through(target, { swap?, land?, color?, duration?,
|
|
1489
|
+
* outDuration? }, position?)` is the GO-THROUGH: dive INTO an element
|
|
1490
|
+
* until its color takes over the frame, world-swap under the cover, pull
|
|
1491
|
+
* out into the new world. Never hand-roll a dive (a magic-scale
|
|
1492
|
+
* camera.move + a hand-timed swap): through() computes the exact cover
|
|
1493
|
+
* scale, measures the target at the dive's own start time, and guarantees
|
|
1494
|
+
* full coverage at the cut. All return `{ duration }` (full span including
|
|
1431
1495
|
* stagger) for sequencing, and accept `(target, effect, opts?, position?)`
|
|
1432
1496
|
* with opts `{ by?: 'chars'|'words'|'lines', duration?, stagger?, ease?,
|
|
1433
1497
|
* from?: 'start'|'center'|'end'|'edges' }`. */
|
|
@@ -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
|
-
##
|
|
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
|
|
23
|
-
with `--claude` instead. If the weekly quota is exhausted, call
|
|
24
|
-
the user the authenticated billing page.
|
|
25
|
-
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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-
|
|
17
|
-
import "./chunk-
|
|
16
|
+
} from "./chunk-KI5E2464.js";
|
|
17
|
+
import "./chunk-H5TR3QJN.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.
|
|
6
|
+
version: "0.1.9",
|
|
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 = "fd0fa24" ? "fd0fa24" : "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
|
};
|
|
@@ -6,14 +6,14 @@ import {
|
|
|
6
6
|
loadConfig,
|
|
7
7
|
recoverCorruptConfig,
|
|
8
8
|
saveConfig
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-KI5E2464.js";
|
|
10
10
|
import {
|
|
11
11
|
PACKAGE_COMMAND,
|
|
12
12
|
PACKAGE_NAME,
|
|
13
13
|
PACKAGE_VERSION,
|
|
14
14
|
PRODUCTION_APP_URL,
|
|
15
15
|
PRODUCTION_SUPABASE_URL
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-H5TR3QJN.js";
|
|
17
17
|
|
|
18
18
|
// src/onboarding.ts
|
|
19
19
|
import {
|
|
@@ -30,6 +30,7 @@ import { homedir } from "node:os";
|
|
|
30
30
|
import { dirname, join } from "node:path";
|
|
31
31
|
import { fileURLToPath } from "node:url";
|
|
32
32
|
import { spawnSync } from "node:child_process";
|
|
33
|
+
import { createRequire } from "node:module";
|
|
33
34
|
var CLAUDE_MARKETPLACE = "Openpod/impractical-agent-plugins";
|
|
34
35
|
var CLAUDE_PLUGIN = "impractical-motion@impractical";
|
|
35
36
|
var CLAUDE_MARKETPLACE_NAME = "impractical";
|
|
@@ -426,7 +427,7 @@ Official setup guide: ${CLAUDE_INSTALL_URL}`
|
|
|
426
427
|
throw new Error(`${label} is required before setup can continue.`);
|
|
427
428
|
}
|
|
428
429
|
}
|
|
429
|
-
function
|
|
430
|
+
function hasSystemChrome() {
|
|
430
431
|
const macPaths = [
|
|
431
432
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
432
433
|
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
@@ -437,11 +438,62 @@ function hasChrome() {
|
|
|
437
438
|
return !result.error && result.status === 0;
|
|
438
439
|
});
|
|
439
440
|
}
|
|
441
|
+
function playwrightCli() {
|
|
442
|
+
try {
|
|
443
|
+
const require_ = createRequire(import.meta.url);
|
|
444
|
+
const cli = join(dirname(require_.resolve("playwright-core/package.json")), "cli.js");
|
|
445
|
+
return existsSync(cli) ? cli : void 0;
|
|
446
|
+
} catch {
|
|
447
|
+
return void 0;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function managedBrowserPath() {
|
|
451
|
+
const cli = playwrightCli();
|
|
452
|
+
if (!cli) return void 0;
|
|
453
|
+
const probe = spawnSync(
|
|
454
|
+
process.execPath,
|
|
455
|
+
[cli, "install", "--dry-run", "chromium-headless-shell"],
|
|
456
|
+
{ encoding: "utf8", windowsHide: true }
|
|
457
|
+
);
|
|
458
|
+
if (probe.error || probe.status !== 0) return void 0;
|
|
459
|
+
const match = /^\s*Install location:\s*(.+?)\s*$/m.exec(probe.stdout ?? "");
|
|
460
|
+
return match?.[1];
|
|
461
|
+
}
|
|
462
|
+
function hasManagedBrowser() {
|
|
463
|
+
const path = managedBrowserPath();
|
|
464
|
+
return Boolean(path && existsSync(path));
|
|
465
|
+
}
|
|
466
|
+
function ensureRenderBrowser(log = () => {
|
|
467
|
+
}) {
|
|
468
|
+
if (hasManagedBrowser()) return;
|
|
469
|
+
const cli = playwrightCli();
|
|
470
|
+
if (!cli) {
|
|
471
|
+
if (hasSystemChrome()) return;
|
|
472
|
+
throw new Error(
|
|
473
|
+
"A browser is required to inspect video frames, and the bundled installer could not be found. Install Google Chrome, or run `npx playwright-core install chromium-headless-shell`."
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
log("Installing a headless browser for frame capture (one time, a few hundred MB)...");
|
|
477
|
+
const result = spawnSync(process.execPath, [cli, "install", "chromium-headless-shell"], {
|
|
478
|
+
stdio: "inherit",
|
|
479
|
+
windowsHide: true
|
|
480
|
+
});
|
|
481
|
+
if (!result.error && result.status === 0) {
|
|
482
|
+
log("Headless browser ready.");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (hasSystemChrome()) {
|
|
486
|
+
log("Could not install the headless browser; falling back to your installed Chrome.");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
throw new Error(
|
|
490
|
+
"Could not install a browser for frame capture, and no Chrome or Chromium was found. Install Google Chrome, or run `npx playwright-core install chromium-headless-shell` and re-run setup."
|
|
491
|
+
);
|
|
492
|
+
}
|
|
440
493
|
function verifyClaudePrerequisites() {
|
|
441
494
|
const major = Number(process.versions.node.split(".")[0]);
|
|
442
495
|
if (!Number.isInteger(major) || major < 22) throw new Error("Node.js 22 or newer is required.");
|
|
443
496
|
requireCommand("claude", "Claude Code");
|
|
444
|
-
if (!hasChrome()) throw new Error("Google Chrome or Chromium is required to inspect video frames.");
|
|
445
497
|
}
|
|
446
498
|
function runClaude(args, log, tolerateAlreadyInstalled = false) {
|
|
447
499
|
const result = commandResult("claude", args);
|
|
@@ -618,6 +670,7 @@ async function setup(options) {
|
|
|
618
670
|
if (options.installAgentMcp) {
|
|
619
671
|
verifyAgentPrerequisites();
|
|
620
672
|
}
|
|
673
|
+
ensureRenderBrowser(options.log);
|
|
621
674
|
const corruptConfigBackup = recoverCorruptConfig();
|
|
622
675
|
if (corruptConfigBackup) {
|
|
623
676
|
options.log(`Backed up invalid configuration to ${corruptConfigBackup}; starting clean.`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
setup
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NG3Q3BYW.js";
|
|
5
5
|
import {
|
|
6
6
|
invoke,
|
|
7
7
|
invokeAt,
|
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
saveConfig,
|
|
12
12
|
studioLink,
|
|
13
13
|
waitForJobs
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-KI5E2464.js";
|
|
15
|
+
import "./chunk-H5TR3QJN.js";
|
|
16
16
|
import "./chunk-77HVPD4G.js";
|
|
17
17
|
|
|
18
18
|
// src/commands.ts
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
PACKAGE_VERSION
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-H5TR3QJN.js";
|
|
5
5
|
import "./chunk-77HVPD4G.js";
|
|
6
6
|
|
|
7
7
|
// src/cli-args.ts
|
|
@@ -140,11 +140,11 @@ async function runCli(args, deps) {
|
|
|
140
140
|
// src/index.ts
|
|
141
141
|
runCli(process.argv.slice(2), {
|
|
142
142
|
writeLine: (line) => console.log(line),
|
|
143
|
-
loadCommands: () => import("./commands-
|
|
144
|
-
loadApi: () => import("./api-
|
|
145
|
-
loadMcp: () => import("./stdio-
|
|
143
|
+
loadCommands: () => import("./commands-56WH3ZVJ.js"),
|
|
144
|
+
loadApi: () => import("./api-NVZQJBVP.js"),
|
|
145
|
+
loadMcp: () => import("./stdio-MJ4LTXME.js")
|
|
146
146
|
}).catch(async (error) => {
|
|
147
|
-
const { ApiError } = await import("./api-
|
|
147
|
+
const { ApiError } = await import("./api-NVZQJBVP.js");
|
|
148
148
|
if (error instanceof ApiError) {
|
|
149
149
|
console.error(`Error (${error.status} from ${error.fn}): ${error.message}`);
|
|
150
150
|
if (error.status === 429) console.error("Weekly MCP allowance reached. Upgrade or wait for Monday UTC.");
|