Command-Line Interface
Clipthesis ships a headless CLI that reuses the same service layer as the desktop app, reading and writing the same database. It is designed to be driven by an automation agent — most usefully Claude Code as a tagging and search assistant for your footage: Claude can list clips that still need tags, see a clip by reading extracted frames, read transcripts, and apply a controlled tag vocabulary.
Every command prints a single JSON object to stdout, so output is trivial to parse.
Build & install
The CLI is built from source into a single self-contained file:
npm run build:cli # → out/cli/clipthesis-cli.mjs (has a #!/usr/bin/env node shebang)Run it directly, or put a clipthesis command on your PATH:
# Run the built file directly
node out/cli/clipthesis-cli.mjs status
# Or link the `clipthesis` bin globally, then call it anywhere
npm link
clipthesis status
# Or via npx without linking
npx clipthesis statusBy default the CLI reads the same database the app uses (~/Library/Application Support/Clipthesis/). Point it elsewhere with --db.
Run writes while the app is closed
The CLI and the desktop app share one SQLite database. Heavy writes (tagging, deletes) are safest with the app closed; a locked database surfaces as a DB_LOCKED error rather than corrupting anything.
Output contract
Every invocation emits one JSON envelope:
{ "success": true, "data": { /* ... */ }, "meta": { /* optional */ } }{ "success": false, "error": { "code": "NOT_FOUND", "message": "Media 'abc' not found" } }data— the result payload (object or array).meta— pagination and context (nextCursor,totalCount,query, …) on list-style commands.- Exit code —
0on success,1on any error envelope.
Error codes
| Code | Meaning |
|---|---|
NOT_FOUND | The requested entity or accessible file does not exist |
ALREADY_EXISTS | A uniqueness constraint was violated (e.g. duplicate tag/group name) |
VALIDATION_ERROR | Bad arguments (missing --force, out-of-range value, invalid color) |
DB_LOCKED | The database is locked — the app is mid-write |
DB_ERROR | A lower-level SQLite error |
UNKNOWN | Anything uncategorized |
Global options
| Option | Description |
|---|---|
--db <path> | Override the data directory (defaults to the app's) |
--format <json|table> | json (default) for machines, table for humans |
--pretty | Pretty-print JSON output |
Diagnostic logs (ffmpeg progress, etc.) go to stderr, so stdout stays a clean JSON stream even while frames are being generated.
Letting Claude see the footage
Scrub frames are not generated during indexing, so the CLI extracts them on demand and caches them. The returned paths are absolute image files an agent reads with its image tooling.
media frames <media-id>
Generate (and cache) a strip of evenly-spaced frames across the clip.
clipthesis media frames <media-id> --count 12| Option | Description |
|---|---|
--count <n> | Number of frames, 2–60 (default 12) |
--regenerate | Re-extract even if frames are already cached |
{ "success": true, "data": {
"thumbnail": "/…/thumbnails/<hash>.jpg",
"frames": ["/…/scrubs/<hash>/frame-0001.jpg", "…"],
"count": 12,
"generated": true
} }generated is false when cached frames were reused. Generation requires a connected drive with the file reachable; otherwise the command returns NOT_FOUND.
media frame <media-id>
Extract a single frame at a position — handy with a transcript timecode ("show me the frame where they said X").
| Option | Description |
|---|---|
--at <ratio> | Position as a 0..1 fraction of the clip duration |
--at-seconds <sec> | Position in seconds (converted via the clip's duration) |
--output <path> | Write to this path instead of the per-clip cache |
With neither flag it extracts a representative mid-clip frame (0.5). --at and --at-seconds are mutually exclusive.
clipthesis media frame <media-id> --at-seconds 42
# → { "success": true, "data": { "path": "/…/scrubs/<hash>/at-….jpg" } }Searching & finding what needs work
search <query>
Tag search with AND logic across space-separated tag names. Add --text to also require a unified match (filename or tag name or transcript).
clipthesis search "beach drone" # clips tagged beach AND drone
clipthesis search "beach" --text "golden hour" # …that also mention "golden hour"| Option | Description |
|---|---|
--text <query> | Also require a unified filename/tag/transcript match |
--exclude-tag <name> | Exclude a tag name (repeatable) |
--type <video|image> | Filter by media type (repeatable) |
--transcript <yes|no> · --audio <yes|no> | Presence filters |
--limit <n> · --cursor <c> | Pagination |
media list / media count
media list is the full browse query; media count returns just the matching total. Both accept the discovery filters that power a tagging workflow:
| Option | Description |
|---|---|
--text <query> | Unified search across filename, tag names, and transcripts |
--missing-required | Only clips missing a required-group tag — the "needs tagging" queue |
--uncollected | Only clips not in any collection |
--tag-token <token> | Group-aware include, e.g. place:beach (repeatable, AND) |
--exclude-tag <name> | Exclude a tag name (repeatable) |
--tag <id> · --drive <id> · --collection <id> · --type <t> | Id/type filters (repeatable) |
--min-duration / --max-duration | Duration bounds (seconds) |
--sort <field> · --sort-dir <asc|desc> · --limit · --cursor | Ordering & pagination |
# What still needs tags?
clipthesis media list --missing-required --limit 20
clipthesis media count --missing-requiredTagging
media tag <media-id> <tag-name>
Add a tag, creating it if needed. A group:value shortcut auto-files the tag into that group (creating the group if missing):
clipthesis media tag <media-id> drone
clipthesis media tag <media-id> place:beach # tag "beach" in group "place"Related: media tags <media-id> (list), media untag <media-id> <tag-id> (remove), media batch-tag <name> --ids a,b,c.
tags group <tag-id> <group-id>
Move an existing tag into a group, or pass none to ungroup it:
clipthesis tags group <tag-id> <group-id>
clipthesis tags group <tag-id> noneTag-group taxonomy
Tag groups are the controlled vocabulary. Marking a group required is what populates the --missing-required queue: any clip lacking a tag from a required group is "needs tagging".
clipthesis tag-groups create place --required
clipthesis tag-groups list| Command | Description |
|---|---|
tag-groups list | All groups with tag counts |
tag-groups get <id> | A single group |
tag-groups create <name> [--color <c>] [--description <t>] [--required] | Create a group (color defaults to blue) |
tag-groups update <id> [--name] [--color] [--description] [--required <bool>] | Update fields |
tag-groups delete <id> [--cascade] --force | Delete; child tags are orphaned unless --cascade |
A tagging session, end to end
# 1. Define the vocabulary once
clipthesis tag-groups create place --required
clipthesis tag-groups create subject --required
# 2. Pull the next clip that needs tags
clipthesis media list --missing-required --limit 1
# 3. Look at it — generate frames, then read the returned image paths
clipthesis media frames <media-id> --count 12
# (Claude reads frames[…] and, if useful, the transcript)
clipthesis transcripts get <media-id>
# 4. Apply tags from the controlled vocabulary
clipthesis media tag <media-id> place:beach
clipthesis media tag <media-id> subject:surfer
# 5. Confirm the clip dropped out of the queue
clipthesis media count --missing-requiredSee also: Tags, Tag Groups, and Search & Filters.