The missing CLI and MCP server for Camtasia.
Programmatic Camtasia editing toolkit. A Camtasia .cmproj bundle's
timeline is plain JSON (project.tscproj); camkit reads and rewrites it
directly — no ffmpeg re-encoding, Camtasia stays the final renderer.
Platform: the core timeline and media commands are cross-platform; the Camtasia app-control commands (
status,open,close,docs) drive the macOS app via AppleScript and are macOS-only.
Extracted from a working rough-cut workflow: record long takes → transcribe
with word-level Whisper timestamps → pick the best takes → camkit rebuild
rewrites the timeline to keep only the good segments.
- @camkit/core — pure cross-platform logic: tscproj load/mutate, editRate time math (705600000 units/s at the project level), clips/sources listing, rebuild planning (per-(src,track) template dedup keeps two-track screen+camera recordings in sync), transcript + silence parsing. Functions return structured data; no console output, no platform calls.
- @camkit/darwin — macOS Camtasia app control via AppleScript: open
documents list, status, open, close-with-save. Camtasia never re-reads
project.tscproj while a document is open, so the edit cycle is
close (saves) → edit JSON → open (reloads). Throws on other platforms. - @camkit/cli — the
camkitbinary: info, clips, sources, rebuild, export-audio, export-video, captions, silences, transcribe, status, close, open, docs. Rebuild always backs up to.bakand refuses to run with a~project.tscprojlock or an existing backup unless--force. Always--dry-runfirst.export-audioflat-mixes the timeline's audio to one file (m4a/wav/flac/…) for cleanup in Audacity/Auphonic — pure ffmpeg, honours track mute and per-clip gain (--rawto bypass).captionsinjects an animated Dynamic Caption track straight into the project from a transcript (same backup/lock safety as rebuild).export-videorenders the current timeline to a ProRes 422.movby driving Camtasia's GUI export (Camtasia stays the renderer — effects, transitions, Dynamic Captions and.trecvideo streams need its engine, no ffmpeg re-encode). macOS-only; needs Accessibility permission. Brittle: it drives Camtasia's export menus via UI scripting, verified only on Camtasia 2026.1.3 — menu renames or a Welcome window can break it. Prefer a short smoke export after Camtasia updates. - @camkit/mcp — placeholder; will wrap core later.
- Bun ≥ 1.x
- ffmpeg on PATH — required by
camkit export-audio,silences, andtranscribe(brew install ffmpeg) - A transcription engine — required by
camkit transcribeonly; see Transcription engines.OPENAI_API_KEY(cloud),REPLICATE_API_TOKEN(hosted), orwhisper-cpp(local,brew install whisper-cpp). - macOS + Camtasia — required by
status/close/open/docsandexport-video; everything else is cross-platform - Accessibility permission — required by
camkit export-videoonly. It drives Camtasia's export GUI via System Events, so grant your terminal under System Settings ▸ Privacy & Security ▸ Accessibility.
- Transcription engines — engine precedence (OpenAI, Replicate, local whisper.cpp), model overrides.
- Captions in the Camtasia UI — higher-quality captions, Dynamic Caption presets, SRT import.
- Why
export-videodrives the GUI — why rendering can't be done from JSON, the export approaches tried, and why UI scripting is the only working path.
bun install
bun test # unit tests (time math, rebuild planning, silence parsing)
bun run typecheck
bun run build # standalone executable → packages/cli/dist/camkit
bun packages/cli/src/camkit.ts info --project path/to/foo.cmproj
# or put the bin on your PATH (it's the TS source, run by bun via shebang):
ln -sf "$PWD/packages/cli/src/camkit.ts" ~/.bun/bin/camkit
camkit --helpSee packages/cli/README.md for full command documentation; every command
also has camkit <command> --help.
.trecrecordings are QuickTime containers: ffmpeg reads the audio and h264 streams but cannot decode the tscc2 screen stream.- Whisper folds pauses into stretched word timestamps — always cross-check
kept ranges with
camkit silencesbefore finalizing a cut. - After a rebuild the project is already cut and
.bakholds the original; to recut, restore the.bakfirst or you'll back up the cut file.