SLOPSHOPPER

cav-review

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

newbandtoastpromptprocess
★ 3v0.1.0MITupdated 2026-10-08rock3r/cav/plugins/cav-review
A shopper browsing a rack in a slop shop
README

Cav

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.

PartWhat it is
cavA 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-bridgeA small Cavalry script that cav talks to. cav setup installs it.
cavalry skillA 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).

Requirements

  • Cavalry 2.4 or newer. Tested on 2.7.2 and 2.8.0.
  • macOS or Windows. See What was tested for the details on Windows.
  • ffmpeg, for videos with audio and for beat detection (brew install ffmpeg, or winget install Gyan.FFmpeg).

Install

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.

Try it

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.

Commands

CommandWhat it does
cav setup / cav doctorInstall 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 statusExisting 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 mcpAn 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 helpersOffline 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 updateVersions, 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.

Update

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.

How well it works

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 usedTasks passedMean timeMean tokens
cav and the skill8 of 111,177 s1.22 M
cav only8 of 111,025 s0.98 M
the upstream cavalry-mcp server3 of 111,604 s3.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 was tested

WhatmacOSWindows
Install from the release, cav setup, offline commandstestedtested (Windows 11, x64)
cav update from 1.0.0 to 1.0.1testedtested
Driving Cavalry: jobs, sheets, checks, renderstested (65 helper and 14 native-feature checks live)not tested
Claude Code and Codex plugins, with a sandboxed agent through cav relaytestednot tested

Documentation

Development

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

Credits

  • cavalry-mcp by Michael Essandoh (MIT): the bridge script is derived from its bridge, and the token handshake, API reference and docs search ideas come from it. See NOTICE.
  • cavalry-types by Remco Janssen (MIT): API names and signatures.
  • Cavalry is made by Scene Group (now part of Canva). Cav is a community plugin and is not affiliated with Cavalry, Scene Group, or Canva. The Cavalry docs are not included; cav docs update downloads them to your own computer.

Licence: MIT.

Recovery, diagnostics and signed macOS bundles

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.

Source 2 files
hooks/register.tsx 149 lines
1import { 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}
149
types/index.d.ts 22 lines
1// 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