Shows the open cav review notes above the prompt, says when the reviewer sends notes, and can start a turn to handle them.

Make motion graphics in Cavalry, the 2D motion design app, from a terminal. You, or a coding agent working for you, write short scripts. cav runs them inside the Cavalry window that is open on your desktop, shows you contact sheets, checks the result for common mistakes, and renders the video, with music if you want.
| Part | What it is |
|---|---|
cav | A command-line tool for macOS and Windows. It runs JavaScript in Cavalry, renders frames, contact sheets and MP4 videos, inspects scenes, finds the beats and the structure of a music track, draws it as a picture, checks a render against it, and searches the Cavalry API and docs offline. cav guide explains the workflow and has motion design recipes. |
| cav-bridge | A small Cavalry script that cav talks to. cav setup installs it. |
cavalry skill | A short entry point for coding agents. It checks the setup and sends the agent to cav guide. Packaged as a Claude Code plugin and a Codex plugin. |
Cav drives Cavalry through the CLI, not an MCP server. Cavalry always runs on a desktop, so a shell is always there, and a command-line tool costs an agent no context until it asks for help. The one MCP server, cav mcp, only shows the review page inside the chat (see Review in the chat).
brew install ffmpeg, or winget install Gyan.FFmpeg).1. Install cav. On macOS:
curl -fsSL https://raw.githubusercontent.com/rock3r/cav/main/install.sh | sh
On Windows, in PowerShell:
irm https://raw.githubusercontent.com/rock3r/cav/main/install.ps1 | iex
The installer checks the download against the release checksums, then runs cav setup.
2. Start the bridge. In Cavalry, click Scripts > cav-bridge and keep its small window open. Do this once each time you start Cavalry. Then check everything:
cav doctor
3. Optional: add the agent plugin. In Claude Code:
/plugin marketplace add rock3r/cav
/plugin install cavalry@cav
For cav review, Claude Code can also show the open review notes above the prompt and start a turn when the reviewer sends notes: /plugin install cav-review@cav.
In Codex:
codex plugin marketplace add rock3r/cav
codex plugin add cavalry@cav
Other agents: copy plugins/cavalry/skills/cavalry into the agent's skills folder.
Agents in a sandbox. Some agent sandboxes block connections to 127.0.0.1 or writes outside the project. cav doctor then reports blocked: a sandbox. Either allow cav to reach 127.0.0.1:8723, or run cav relay outside the sandbox and let cav work through a folder: see Agents in a sandbox.
cav scene new --width 1920 --height 1080 --fps 60 --seconds 4
cav guide # the workflow, with a first script to copy
cav run title.js # run your script inside Cavalry
cav sheet # look at 12 frames in renders/sheet.png
cav check # find problems a viewer would notice
cav render -o out/title.mp4
The user guide walks through a first animation, music sync and troubleshooting.
| Command | What it does | ||||
|---|---|---|---|---|---|
cav setup / cav doctor | Install or check everything: token, bridge script, Cavalry, ffmpeg, docs, running bridge | ||||
cav guide [topic] | How to work: workflow, design recipes, music sync, traps, native features | ||||
cav status | Existing bridge state; no scene work is queued | ||||
| `cav scene new\ | comp\ | open\ | save` | New scene (refuses to discard unsaved work), change the comp, open, save | |
| `cav run <file.js>... \ | -e <code>` | Run JavaScript in Cavalry with the helper library loaded; prints the return value | |||
cav job wait [<id>] | Wait for one raw job without continuing command phases | ||||
cav operation status/resume <id> | Inspect or continue a complete scene open, frame, sheet, render, check or run after a timeout | ||||
cav tree / cav layer <id> | The layer tree; one layer's position, bounding box and keys | ||||
cav frame [n...] / cav sheet [frames] | Render frames to PNG, or a labelled contact sheet | ||||
cav onion [start-end] | Draw several frames on top of each other to show a motion path and its easing | ||||
cav check [--quick] [--profile] | Structural performance warnings and bounded visual/measured checks | ||||
cav render [-o out.mp4] [--audio track] | Render to MP4, check the frame count, add the music | ||||
cav beats <audio> | Tempo, beats and downbeats as frame numbers, plus sections, rises, silences and accents | ||||
cav spectrogram <audio> | Draw the track: spectrogram, loudness, beats, sections and accents on one time axis | ||||
cav sync <video.mp4> | Measure the cuts and hits of a render against the beats | ||||
cav review [video.mp4] | A review page: step frames, draw, comment, then send the notes to the agent (wait, export, resolve) | ||||
cav mcp | An MCP server (stdio) that shows the review page inside the chat, as an MCP App | ||||
| `cav board init\ | frames\ | sheet\ | animatic\ | mood` | Storyboard timed in beats, frames per shot, a board image, an animatic on the music, mood boards |
| `cav gen image\ | vector` | Generate stills, transparent PNGs and SVG; trace a PNG to SVG | |||
cav ref / `cav sfx search\ | get\ | gen\ | index` | Find reference images, sounds and music under free licences; generate sound effects | |
| `cav music gen\ | render / cav listen <track>` | Generate music planned on the storyboard, or render a written score locally; judge takes: loudness, cuts, picture, local scores, critique | |||
cav score [storyboard.json] | A REAPER project with the render, a marker per shot and the takes, to finish the music by ear | ||||
| `cav credits [check\ | licence]` | Credit lines and licence checks for every asset the project uses | |||
| `cav config [check\ | set-key\ | order\ | model]` | Which service does each job, where its key lives; cav doctor --services tests them | |
cav api / cav docs / cav types / cav helpers | Offline search: API, Cavalry docs, layer types, the helper library | ||||
cav relay --spool <dir> | Forward commands for agents whose sandbox blocks cav | ||||
cav version [--check] / cav update | Versions, and updates that ask before they install |
The production commands work without API keys (greybox frames, Openverse, Wikimedia, written scores, local models through uv, ComfyUI and ACE-Step servers) and use better services when you add keys. cav guide production explains them.
Every command accepts --json. Exit codes: 0 ok, 1 error, 2 bridge not reachable, 3 job still running, 4 bridge lost.
cav update
It shows the new release, asks, checks the download, and runs cav setup. If cav doctor then says the running bridge is older, close the cav-bridge window in Cavalry and start it again. Update the plugin with /plugin marketplace update cav in Claude Code.
We gave the same 11 motion design briefs to an agent with a mid-size model (GLM-5.3 Flash) in three ways, and compared the results with automatic checks and a blind judge (measured on Cavalry 2.8.0):
| Agent used | Tasks passed | Mean time | Mean tokens |
|---|---|---|---|
cav and the skill | 8 of 11 | 1,177 s | 1.22 M |
cav only | 8 of 11 | 1,025 s | 0.98 M |
| the upstream cavalry-mcp server | 3 of 11 | 1,604 s | 3.22 M |
cav did most of the work: with or without the skill, the agent passed far more tasks with a third of the tokens. The skill did not add a measurable gain, so 1.0 moved its guidance into cav guide, where every agent can read it. The method, the full tables and the problems we found are in the evaluation.
| What | macOS | Windows |
|---|---|---|
Install from the release, cav setup, offline commands | tested | tested (Windows 11, x64) |
cav update from 1.0.0 to 1.0.1 | tested | tested |
| Driving Cavalry: jobs, sheets, checks, renders | tested (65 helper and 14 native-feature checks live) | not tested |
Claude Code and Codex plugins, with a sandboxed agent through cav relay | tested | not tested |
go test ./...
node --test assets/helpers/test/*.test.js assets/bridge/test/*.test.js
cav scene new --force --width 1280 --height 720 --fps 30 --seconds 3 # a throwaway scene
cav run assets/helpers/test/live.js # live checks in Cavalry
claude plugin validate . --strict
NOTICE.cav docs update downloads them to your own computer.Licence: MIT.
scene open, frame, sheet, render, check and run return an operation ID. After a timeout, use cav operation status <id> and cav operation resume <id> --timeout 90m instead of retrying the command. cav check --quick --json avoids playhead changes, bounds and renders; --profile adds a small measured frame set. Quick inspection follows referenced pre-comps without switching the active comp; findings include their composition paths. Select later frames with --profile-frames 840,1560,1908. Simulations require chronological PNG warm-up; explicitly allow those extra frames with --max-warmup 2000 and an appropriate timeout. Failures and skipped coverage are explicit. A between-sample profile budget limit remains skipped coverage, including when no sample finishes; it is not an API failure. See operation recovery for states, concurrency and remaining limits.
macOS releases preserve a signed, notarized and stapled Cav.app; the installer exposes its executable on PATH through a symlink. Users of an already-shipped older updater should run the current installer once to migrate. Development builds stay credential-free; real release builds require signing. See notarization.
hooks/register.tsx 149 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { ReviewSummary } from '../types'
5
6// cav review keeps its notes in <renders>/<video>.review.json. This plugin asks Claude Code
7// to watch that folder and reads the newest file when it changes: a band above the prompt
8// shows the open notes and links to the page, a toast says when the reviewer presses "Send to
9// agent", and (option `wake`) a turn starts that reads the notes with `cav review wait`.
10
11const summary = atom({ plugin: 'cav-review', key: 'summary' } as const, null)
12const hidden = atom({ plugin: 'cav-review', key: 'hidden' } as const, false)
13const announced = atom({ plugin: 'cav-review', key: 'announced' } as const, null)
14
15// What `cav review export --json` prints (the fields the band uses).
16type Export = {
17 video: string
18 reviewFile: string
19 notes?: { status: string; onOlderRender?: boolean }[] | null
20 sends?: { n: number; comments: string[]; deliveredAt?: string }[] | null
21 server?: { url: string } | null
22}
23
24function summarize(x: Export): ReviewSummary {
25 const notes = x.notes ?? []
26 const open = notes.filter(n => n.status === 'open')
27 const pending = (x.sends ?? []).find(s => !s.deliveredAt)
28 return {
29 video: x.video,
30 file: x.reviewFile,
31 open: open.length,
32 resolved: notes.length - open.length,
33 olderOpen: open.filter(n => n.onOlderRender).length,
34 pending: pending ? { n: pending.n, notes: pending.comments.length } : null,
35 // Surfaces link only https: and http://localhost; the review server answers both names.
36 url: x.server?.url ? x.server.url.replace('//127.0.0.1:', '//localhost:') : null,
37 }
38}
39
40function wakePrompt(s: ReviewSummary): string {
41 return (
42 `The reviewer pressed "Send to agent" in cav review: ${s.pending?.notes ?? 0} note(s) on ${s.video}. ` +
43 `Run \`cav review wait ${s.video} --timeout 10s\` to read them (it prints each note's frame, text, drawing and snapshot path, ` +
44 `and marks the send received), look at each snapshot, fix the notes, re-render to the same file, then resolve each with ` +
45 `\`cav review resolve <id> --note "what changed"\`.`
46 )
47}
48
49// Reads the newest review file and updates the band. Nothing polls: this runs when the
50// session starts, when Claude Code reports a change to a watched file, and around turns.
51async function refresh($: EngineInterface, folder: string, wake: boolean) {
52 // cav finds the newest review file and reads it, so the format lives in one place.
53 const r = await $.process.run(['cav', 'review', 'export', '--json', '--dir', folder], { timeoutMs: 15000 })
54 let x: Export | null = null
55 if (r.exitCode === 0) {
56 try {
57 x = JSON.parse(r.stdout)
58 } catch {
59 return
60 }
61 }
62 if (x === null || !x.reviewFile) {
63 if ((await read($, summary)) !== null) await update($, summary, () => null)
64 return
65 }
66 const s = summarize(x)
67 const before = await read($, summary)
68 if (JSON.stringify(before) !== JSON.stringify(s)) await update($, summary, () => s)
69
70 if (s.pending) {
71 const key = `${s.file}#${s.pending.n}`
72 if ((await read($, announced)) !== key) {
73 await update($, announced, () => key)
74 await update($, hidden, () => false)
75 $.ui.toast(`cav review: the reviewer sent ${s.pending.notes} note(s) on ${s.video}`, { timeoutMs: 8000 })
76 if (wake) await $.prompt.submit({ text: wakePrompt(s) })
77 }
78 }
79}
80
81export const register: Register = (on, options) => {
82 const folder = typeof options.folder === 'string' && options.folder !== '' ? options.folder : 'renders'
83 const wake = options.wake !== false
84
85 // Ask Claude Code to watch the renders folder and the review files in it.
86 on('classic.SessionStart', async ($, e, next) => {
87 const result = await next(e)
88 const base = `${e.cwd}/${folder}`
89 const paths = [base]
90 try {
91 for (const f of await $.fs.list(folder)) {
92 if (f.kind === 'file' && f.name.endsWith('.review.json')) paths.push(`${base}/${f.name}`)
93 }
94 } catch {
95 // the folder appears later; watching it catches that
96 }
97 return { ...result, watchPaths: [...(result.watchPaths ?? []), ...paths] }
98 }).catch(($, e, next) => next(e))
99
100 on('classic.FileChanged', async ($, e, next) => {
101 if (e.file_path.endsWith('.review.json') || e.file_path.endsWith(`/${folder}`)) await refresh($, folder, wake)
102 return next(e)
103 }).catch(($, e, next) => next(e))
104
105 on('session.start', async ($, e, next) => {
106 await refresh($, folder, wake)
107 return next(e)
108 })
109
110 // A failure here must never hold up the person's prompt.
111 on('prompt.submit', async ($, e, next) => {
112 refresh($, folder, wake).catch(() => {})
113 return next(e)
114 }).catch(($, e, next) => next(e))
115
116 on('turn.complete', async ($, e, next) => {
117 refresh($, folder, wake).catch(() => {})
118 return next(e)
119 }).catch(($, e, next) => next(e))
120
121 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
122 const s = await read($, summary)
123 if (s === null || e.props.hasSurvey || (await read($, hidden)) || (s.open === 0 && !s.pending)) {
124 return next(e)
125 }
126 const { Box, Button, Link, Text } = $.ui.resolve(e)
127 const name = s.video.split('/').pop()
128 return (
129 <Box flexDirection="row" gap={1} flexWrap="wrap">
130 <Text bold>cav review</Text>
131 <Text>
132 {name}: {s.open} open note{s.open === 1 ? '' : 's'}
133 {s.olderOpen > 0 ? ` (${s.olderOpen} on an older render)` : ''}
134 </Text>
135 {s.pending ? (
136 <Text color="yellow">
137 send #{s.pending.n} waiting: {s.pending.notes} note{s.pending.notes === 1 ? '' : 's'}
138 </Text>
139 ) : null}
140 {s.url ? <Link href={s.url} label="open the page" /> : null}
141 {s.pending ? (
142 <Button key="handle" label="Handle notes" onPress={() => $.prompt.submit({ text: wakePrompt(s) })} />
143 ) : null}
144 <Button key="hide" label="Hide" onPress={() => update($, hidden, () => true)} />
145 </Box>
146 )
147 })
148}
149types/index.d.ts 22 lines1// What the band draws: the newest review file in the renders folder.
2export type ReviewSummary = {
3 video: string // the render's path, as cav review recorded it
4 file: string // the review file
5 open: number // open notes
6 resolved: number
7 olderOpen: number // open notes made on an older render
8 pending: { n: number; notes: number } | null // a send no agent has picked up
9 url: string | null // the running review page, when cav review serves it
10}
11
12declare module 'claude-code' {
13 interface PluginState {
14 'cav-review': {
15 summary: ReviewSummary | null
16 hidden: boolean
17 // The last send announced, so a send wakes the session once.
18 announced: string | null
19 }
20 }
21}
22