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 code — 0 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|audio>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

Finding where footage lives (and lived) ​

When files move between drives, Clipthesis keeps the old paths as location history (see Location History). These two commands make that history searchable — the building blocks for "this old project references a path that no longer exists; where is that file now?"

media history <media-id> ​

Every location a clip has ever had. Active locations come first (newest first), then removed ones (most recently removed first).

bash
clipthesis media history <media-id>
# → {
#     "success": true,
#     "data": {
#       "media": { "id": "…", "filename": "sunset.mp4", "master_hash": "…" },
#       "locations": [
#         { "id": "…", "status": "active",  "drive_name": "8TB",
#           "file_path": "footage/day1/sunset.mp4",
#           "absolute_path": "/Volumes/8TB/footage/day1/sunset.mp4", … },
#         { "id": "…", "status": "removed", "drive_name": "T7",
#           "file_path": "footage/day1/sunset.mp4",
#           "absolute_path": "/Volumes/T7/footage/day1/sunset.mp4",
#           "deleted_at": "2026-08-01T…", … }
#       ]
#     },
#     "meta": { "activeCount": 1, "removedCount": 1 }
#   }

Unknown ids return NOT_FOUND.

media locate <path> ​

Resolve an absolute file path — including one that no longer exists — to the media that lives or lived there. Each match pairs the location that matched with the media's current live locations, so a dead path answers with where the file is now.

OptionDescription
--limit <n>Maximum matches to return, 1–100 (default 10)
--current-onlyOnly match live locations, ignoring removed ones

Matching runs in tiers, best first: the path falls under a known drive root (drive-root), the /Volumes/<name>/… volume name matches a drive's name (volume-name), or the filename matches anywhere, scored by how many trailing path segments agree (suffix). Comparison is case-insensitive and Unicode-normalization-safe. No matches is a success with an empty list, not an error.

bash
clipthesis media locate "/Volumes/T7/footage/day1/sunset.mp4"
# → {
#     "success": true,
#     "data": {
#       "query": "/Volumes/T7/footage/day1/sunset.mp4",
#       "matches": [
#         {
#           "media": { "id": "…", "filename": "sunset.mp4", "master_hash": "…" },
#           "match_type": "drive-root",
#           "matched_location": { "status": "removed", "drive_name": "T7", … },
#           "current_locations": [
#             { "drive_name": "8TB",
#               "absolute_path": "/Volumes/8TB/footage/day1/sunset.mp4", … }
#           ]
#         }
#       ]
#     },
#     "meta": { "matchCount": 1 }
#   }

Agent example — repairing an old DaVinci Resolve project. A .drp/.xml from last year references clips by absolute T7 paths, but that footage moved to the 8TB archive. An agent can extract each media path from the project, then:

bash
# For each dead path in the project:
clipthesis media locate "/Volumes/T7/footage/day1/sunset.mp4" --limit 1
# matched_location.status == "removed" tells you the path is historical;
# current_locations[0].absolute_path is the relink target on the 8TB.

Loop that over every missing clip and you have an old-path → new-path relink map without touching Finder.

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.