# tcut — terminal sessions to video (for agents) # Skills: npx skills add AmanVarshney01/tcut (tcut + tcut-remotion) npm package: termcut · command: tcut · requires Bun ≥ 1.4 (or the standalone binary) · ffmpeg only for .mp4/.gif/.webm/.webp · Linux: Chrome/Chromium on PATH (or BUN_CHROME_PATH) for pixel output; svg/html/txt need neither Docs: https://github.com/AmanVarshney01/tcut · Reference: https://github.com/AmanVarshney01/tcut/blob/main/packages/tcut/docs/REFERENCE.md ## When to use tcut Reach for tcut when the job is any of these: - a terminal demo video or GIF for a README, docs page, release note or tweet (record once, render mp4/gif/webm/svg/html from the same .cast) - screenshots of CLI/TUI output that must stay in sync with the code (`t.snapshot("x.png")` inside a script, re-rendered in CI) - driving and asserting on an interactive TUI or wizard the way a user would (`t.wait`, `t.expect`, arrow keys, `tcut test` as a test runner) - recording an AI coding agent session (Claude Code, Codex, OpenCode) end to end - a launch/tour video: tcut recordings as clips, composed with Remotion (`tcut-remotion` skill) Do not reach for tcut to screen-record a GUI, to capture a terminal you cannot run commands in, or when a plain text transcript is enough (use `tcut render x.cast -o x.txt` only if a recording already exists). How an agent should call it: write a `.video.ts` with `defineVideo`, run `tcut --json`, read the JSON result; never rely on timing sleeps when `t.wait`/`t.expect` can observe the screen. ## Library (import from "termcut", Bun only) defineVideo(config, script) → Video: await video.run({ force, log }) → { outputs, screenshots, durationSeconds, recording }; video.record(); video.render(recording, { overrides, clip }); renderCast(file, overrides, onProgress?, clip?). Also: recordLive, cutRecording/concatRecordings/selectChapters, buildSvg/buildHtml/replayFrames, diffCasts, diagnoseCast, generateScript, runScriptTests, publishFiles, readCast/writeCast, resolveConfig, themes. In CI pass explicit theme/font (auto reads the running terminal); render one video at a time. ## Non-interactive contract - Every command exits 0 on success, 1 on failure. No prompts except `tcut publish --setup` without flags. - Add `--json` to get exactly one JSON document on stdout (results, or `{ "error": "...", "type": "WaitTimeoutError" | "ExpectationError" | ... }`). Progress/status lines are suppressed. - `-q` silences status lines without JSON. Status goes to stdout, errors to stderr (unless --json). - Recording is cached: re-running an unchanged script reuses its .cast; `--force` re-records. - `tcut rec` needs a TTY to pass keystrokes through; agents should prefer scripts or `tcut rec -- ` for non-interactive commands. `tcut rec` opens the user's own shell (parent process, else $SHELL) with its config; `--clean` gives a plain `>` prompt shell. `-- ` runs through that shell (aliases, functions, fish abbreviations, colours); `--raw` runs the binary directly. ## Commands tcut [-o out…] [--theme t] [--width px --height px] [--json] record + render tcut record cast only tcut render [-o out…] [--theme t] [--speed x] [--json] re-render (no shell is run) tcut rec [-- command…] [-o out…] [--browser url] live session (TTY); writes .cast + editable .video.ts tcut test [--json] run scripts fast (no sleeps/typing delay), exit 1 if any expect() fails tcut diff a.cast b.cast [--at s] [--images dir] [--json] screen-text regression check, exit 1 on difference tcut doctor f.cast [--json] features the program used + what tcut cannot show (inline images, unknown escape sequences) tcut cut f.cast --from 2s --to 10s | --chapters a,b [--cast out] [-o …] keep part of a recording (visible timeline); --split-chapters on render writes one file per chapter tcut concat a.cast b.cast… [--gap 500ms] [--cast out] [-o …] join same-size recordings; screen resets at each seam tcut publish [--json] · tcut publish --setup --endpoint U --bucket B --access-key K --secret-key S S3-compatible upload tcut init [name] [--template basic|tour|test] · tcut themes [query] Outputs by extension: .mp4 .webm .gif .webp (ffmpeg) · .svg .html (no deps) · .png .jpg (final frame) · .txt (final screen as text) · .log (full transcript incl. scrollback) · dir/ (PNG frames) Render flags: --from/--to (seconds or "1.5s"), --chapters a,b, --split-chapters, --shadow, --watermark "text", --watermark-image logo.png, --margin-fill transparent (alpha in png/webp/gif/webm/svg/html; mp4 falls back) ## Script shape import { defineVideo } from "tcut"; export default defineVideo( { output: ["demo.mp4", "demo.gif"], cols: 80, rows: 24, theme: "catppuccin-mocha", typingSpeed: "40ms" }, async (t) => { await t.run("ls -la"); // type + Enter + wait for the prompt to return (screen-aware) await t.expect(/total \d+/); // assert on the rendered screen; throws ExpectationError with a dump await t.type("vim x"); await t.enter(); await t.wait(/~/, { scope: "screen" }); // TUIs: wait on screen text await t.escape(); await t.type(":q!\n"); await t.wait(); await t.hide(async () => { await t.run("rm x"); }); // runs, but is cut from the video await t.sleep("1s"); }, ); t methods: run type paste(bracketed when the program asked) enter tab backspace delete escape space up down left right home end pageUp pageDown (SS3 form in application cursor mode) ctrl("c") alt("b") shift("tab") scrollUp scrollDown key("f5") raw sleep wait expect(re, {scope: "line"|"screen"|"scrollback"}) hide timelapse(fn,{speed}) snapshot(path: .png pixel still, .svg vector still of this exact moment, written on every render; screenshot=alias) marker resize clear print(markdown; [text](url) becomes a real hyperlink) title(text) zoom({rows,cols}) zoom(null) chapter(name) focus(target) screen() scrollback() line() cursor() Timelapse: `await t.timelapse(() => t.run("bun install"), { speed: 8 })` plays the install 8× faster (maxPause only removes silence; timelapse compresses output). Chapters are cut points for --chapters/--split-chapters. Captions: print("## Step 1\nSome **bold**") shows rendered Markdown in the terminal (written to the recording only, never typed into the shell); title("Text") = big heading + rule + pause. Call them at a shell prompt. Config: output shell("bash"|"zsh"|"fish"|"sh" clean with known prompt | "user" = the launching shell with its config | string[] command) prompt promptPattern(regex for run(); auto-detected by tcut rec for "user") cols rows width height requires(["bun","eza"] — fail fast before recording if a tool is missing) preset(readme|x|youtube|square) maxPause(idle compression) keys(key overlay; boolean or {limit, font, color, background, radius}, one chip by default) fps typingSpeed typingJitter seed playbackSpeed waitTimeout endPause quantize core cache font({family,size,lineHeight,letterSpacing} | "auto" = the launching terminal's font) theme(name | object | "auto" = the launching terminal's colours via OSC queries or its config; tcut rec defaults both to auto in a TTY) cursor{blink,period} title("auto" follows OSC titles) padding margin marginFill("transparent" allowed) shadow(true | {x,y,blur,color,opacity}; margin defaults to 40) watermark("text" | {text|image,position,opacity,size,color,margin}) borderRadius windowBar title loopOffset cast browser{url,width,height,fps,title,position:right|left|top|bottom|overlay,offset{x,y}} Browser pane (scripted mode): t.browser.goto(url) / waitFor(/text/) / click(sel) / reload() / evaluate(js); t.focus("browser"|"terminal") for overlay z-order. Composited into mp4/gif/png only. Start dev servers as `cmd /tmp/dev.log 2>&1 &` (logs would repaint over TUIs; stdin must be detached or the job stops). ## Writing robust scripts for TUIs - Wait for what you will react to: `await t.wait(/Choose a framework/, { scope: "screen" })` before pressing keys. - Select by reading: `while (!/●\s+Cloudflare/.test(t.screen())) await t.up();` then `await t.enter()`. - Agents in TUIs (claude, codex): wait for their trust prompt, press Enter, wait for the input box, type, then wait for a completion marker (e.g. /✻ \w+ for \d+s/ for Claude Code; absence of "esc to interrupt" for Codex), then `/exit` or `/quit` and `await t.wait()`. - After a full-screen program exits the primary screen may hold stale text; prompt detection is cursor-relative, so `run()`/`wait()` still work. Use `t.hide(() => t.clear())` to tidy the video. - Failures print the whole screen — read it; it tells you which prompt you are actually on.