Writing / Product

Sep 5, 2026

Introducing DemoKit

Dekai Li · 4 min read

Today I'm releasing DemoKit, a command-line demo recorder. It reads your codebase, plans the demo, drives the real app in a browser, films it in 4K with the camera ported from Cap — and refuses to write the file unless the feature it was demonstrating actually worked.

Amazon, given an errand: find a well-reviewed mechanical keyboard and put it in the cart. Search, filter to four stars and up, open the top result, read the reviews, add it, decline the protection-plan upsell, end on a cart with a real subtotal. One take, 7/7 verified · 3840×2160 · 31s.

Every demo video is a small lie of omission. You record the take where it worked, quietly delete the four where it didn't, and ship the fifth. Six weeks later the video still shows a green checkmark long after the feature underneath it stopped producing one. That's the failure mode DemoKit is built around, so the recorder carries the claim with it — this click was supposed to open the diff and show what changed — and when the take is over, three independent checks decide whether that happened.

The pipeline

DemoKit doesn't hunt through your UI hoping to find something worth filming. It reads the routes, the components and the tests first, writes a flow file, and only then opens a browser — the way a forward-deployed engineer learns a product before demoing it to the customer.

Four stages, one command, nothing to click

01  Plan
Read the routes, the components and the tests; decide what the demo has to prove before opening anything.
→ flow.json
02  Capture
Drive the real app in headless Chromium, signed in as you when it needs it; every press, release and repaint timestamped.
→ events.json
03  Render
Cap's camera: spring zoom on the click, macOS cursor, your own wallpaper behind the window.
→ demo.mp4 · 4K
04  Verify
Three checks per step; no file if they disagree.
→ verify.json
Given a goal rather than a script — research Ada Lovelace and trace her work back to the machine it was written for — the agent planned the path and walked it: Lovelace, Babbage, the Analytical Engine, the Difference Engine. Five pages, one take, every hop verified · 31.2s.

The camera

A model deciding where to push in is the worst of both worlds: slow, inconsistent, and wrong often enough that you stop trusting the output. So DemoKit doesn't decide. It ports Cap's camera whole — the same segment generation, the same spring solve, the same cursor — and lets the recorded clicks drive it.

Two frames from the take above, two seconds apart: 1.00× and 2.00×. Nobody chose either framing — the click generated the segment and the spring did the rest, three times over thirty-one seconds, without drifting.

Clicks arrive as press-and-release pairs, not as points. Cap anchors the end of a zoom segment to the release; feed it only the press and every segment comes out about 140 ms short, which is exactly the bug that made my first four attempts feel wrong without being obviously wrong. The camera position is then the analytic solution of a damped spring, evaluated every 8 ms. It never overshoots into a hard stop, and it never parks at 2× while nothing is happening.

The constants, as shipped by Cap

PRE_PADDING
300 ms of runway before the press
POST_PADDING
2 500 ms held after the release
MERGE_GAP
two segments closer than 2 500 ms become one move
ZOOM_STEP
the spring is solved every 8 ms, not every frame
SCREEN_SPRING
stiffness 200 · damping 40 · mass 2.25
CURSOR_SPRING
stiffness 470 · damping 3 · mass 70 — the mellow one

None of that is tuned to one site. Same binary, same command, pointed at somewhere nobody involved controls: typing into Wikipedia's search box and landing on the article about the exact spring the camera is running on.

DemoKit typing on en.wikipedia.org. The suggestion list is a real one, and the article it opens describes underdamped, critically damped and overdamped motion — the three cases the camera solves for. 11.7s · 2/2 verified.

Harder targets

A recorder that only survives server-rendered pages isn't much use. The apps worth demoing stream, virtualise, and draw themselves on a canvas. Three of the more hostile targets on the public web, driven and verified without a single fixture.

Google Maps, given a task: get from the Ferry Building to the Golden Gate Bridge. It types both ends, reads the routes off a WebGL canvas streaming vector tiles, switches to walking and opens the turn-by-turn. Two runs, same result. 24.9s · 4/4 verified, twice.
Grafana's public playground: dark theme, virtualised search, panels that only draw once their queries come back. The search step moved only 1.54% of the source pixels — one row in a long list — and passed because the delivered cut carried it: 19.63% between rest points. 12.8s · 2/2 verified.
Forty-two seconds, one take, no cut: a reviewer moving through a merged pull request on vercel/next.js, out to the issue tracker, into the open queue, and down into the diff of whatever happened to be at the top that morning. Eight steps, eight camera moves, 8/8 verified.

Verification

Encoding succeeded, the click landed, the DOM moved — none of those mean the thing you were demonstrating works. So every step is judged three times, from three different vantage points, and all three have to agree before a file is written.

Three checks per step, from one real run

STATE

Did the product change, and change into the right thing? Word-shingle novelty against the page before the click, plus the assertion the plan wrote down.

93.5% new · rows 0→6 · "Changes from" on screen
SOURCE

Did the pixels move, in the region it claimed? Measured on the raw capture, before any camera work.

33.19% of a 2880×1600 area at 0,20 — the floor is 0.40%
DELIVERED

Can you actually see it in the cut you're shipping? Sampled only where the camera is at rest, so a change mid-move or off-frame does not count.

10.21% between rest points · 8.77s of screen time
The evidence DemoKit writes for itself, straight out of the Amazon run: the delivered frames either side of the add-to-cart beat — the buy box before, a cart with a real subtotal after — sampled only where the camera is at rest. Not a screenshot of the tool; an artefact of the tool.

The interesting run is the one that fails. Here is the same Wikipedia flow with one extra beat that films a control which does nothing — clicking the tab the page is already on. Two steps prove themselves. The third can't, and so there is no file.

VERIFIED   2.27s  search for the thing the camera is built on
   [dom]    yes  7.5% of the page content is new, rows 1->12
   [dom]    yes  "oscillating physical system" appears - on screen now

VERIFIED   6.89s  open the article
   [dom]    yes  97.7% of the page content is new, url changed
   [dom]    yes  "critically damped" appears - on screen now

FAILED    11.89s  switch to the reading view
   [dom]    NO   0.0% of the page content is new, rows 6->6, url same
                 - below 8%, so nothing a viewer would notice happened here
   [dom]    NO   "From Wikipedia, the free encyclopedia" appears - it was
                 already on screen before the step - this proves nothing
   [source] yes  0.40% of the window differs, in a 139x1057 area at 1717,160

outcome: failed  ({"verified":2,"failed":1,"inconclusive":0})

Real output, unedited apart from two lines that pointed at file paths. Note the last check: the pixels did move. It still failed, because moving pixels is not the same as showing someone something.

Where it sits

This is a crowded shelf, and pretending otherwise would be the same dishonesty the tool is built to prevent. Vercel Labs ships WebReel, which records scripted browser demos from JSON with an animated cursor. aidemo is closer still: an agent writes a storyboard and it renders 1080p with auto-zoom, narration and captions. Both are good. Neither is trying to answer the question this one is.

I ran the closest one on the same errand as the video at the top, same day, same steps. WebReel could not get past the search: its documented type shape errored on a text/selector collision, and in two further variants the click landed but the page never navigated, so it aborted with no video at all. DemoKit hit the same wall on the search button, refused to write a file, named the step, and after the search was moved to the suggestion click, verified all seven. Three attempts, zero videos, versus one refusal and then 7/7.

What proves the feature worked

ToolWhat comes outWhat proves it A human + Screen Studio4K, smoothed cursor, auto-zoomnothing — you watched it, and so must everyone else Playwright recordVideoWebM VP8, no cursorasserts the test; never looks at the video WebReel (Vercel Labs)MP4/GIF, animated cursornothing aidemo1080p, auto-zoom, narrationdiffs against a committed baseline — drift, not brokenness Skyvern / Browser Usesession replay, for debuggingvalidates its own goal, then discards the recording DemoKit3840×2160, real macOS cursor, Cap's spring camerathree checks per step, and no file if they disagree

Where it loses, plainly: no narration, no captions, no music. It is AGPL-3.0 because the camera is ported from Cap, which is a real adoption cost next to MIT competitors. It cannot attach to the Chrome you already have open — nothing can, since Chrome 136 — so it needs one demokit login. macOS and Linux only. And it is one person's tool, not a company's.

Start recording

DemoKit is available now. One line, no Docker, no daemon, no account.

$ npm i -g @dekai/demokit
$ demokit local flows/amazon-task.json out/demo.mp4
  verified 7/7 · out/demo.mp4 · 3840×2160

First run downloads a headless Chromium shell (~200 MB) and, only if your Python is missing Pillow or numpy, builds a venv in the cache. After that there is nothing to start and nothing to keep running. Every flow on this page ships in the repo, so you can run the exact recordings above against the exact same URLs.

Cap's MIT exception covers only its cap-camera* and scap-* crates; crates/rendering is AGPLv3, and so is DemoKit. Versions 0.1.0 through 0.3.0 shipped under MIT by mistake and are deprecated on npm. The NOTICE names every ported function and constant against the Cap file it came from. Source on GitHub, package on npm.

Filed under: ProductAuthor: Dekai Li