Skip to content

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:

bash
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:

bash
# 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 status

By 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:

json
{ "success": true, "data": { /* ... */ }, "meta": { /* optional */ } }
json
{ "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 code0 on success, 1 on any error envelope.

Error codes

CodeMeaning
NOT_FOUNDThe requested entity or accessible file does not exist
ALREADY_EXISTSA uniqueness constraint was violated (e.g. duplicate tag/group name)
VALIDATION_ERRORBad arguments (missing --force, out-of-range value, invalid color)
DB_LOCKEDThe database is locked — the app is mid-write
DB_ERRORA lower-level SQLite error
UNKNOWNAnything uncategorized

Global options

OptionDescription
--db <path>Override the data directory (defaults to the app's)
--format <json|table>json (default) for machines, table for humans
--prettyPretty-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.

bash
clipthesis media frames <media-id> --count 12
OptionDescription
--count <n>Number of frames, 2–60 (default 12)
--regenerateRe-extract even if frames are already cached
json
{ "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").

OptionDescription
--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.

bash
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).

bash
clipthesis search "beach drone"                 # clips tagged beach AND drone
clipthesis search "beach" --text "golden hour"  # …that also mention "golden hour"
OptionDescription
--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:

OptionDescription
--text <query>Unified search across filename, tag names, and transcripts
--missing-requiredOnly clips missing a required-group tag — the "needs tagging" queue
--uncollectedOnly 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-durationDuration bounds (seconds)
--sort <field> · --sort-dir <asc|desc> · --limit · --cursorOrdering & pagination
bash
# What still needs tags?
clipthesis media list --missing-required --limit 20
clipthesis media count --missing-required

Tagging

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):

bash
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:

bash
clipthesis tags group <tag-id> <group-id>
clipthesis tags group <tag-id> none

Tag-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".

bash
clipthesis tag-groups create place --required
clipthesis tag-groups list
CommandDescription
tag-groups listAll 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] --forceDelete; child tags are orphaned unless --cascade

A tagging session, end to end

bash
# 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-required

See also: Tags, Tag Groups, and Search & Filters.

Released under the MIT License.