ClawGallery
ClawGallery is a Rust CLI for an agent-native screenshot gallery. It registers screenshot/image folders, bootstraps and polls for new images, stores metadata in JSONL, can call a visual-understanding model for titles/captions, safely renames files, and supports keyword search.
Supported image extensions are png, jpg, jpeg, webp, avif, gif, heic, and heif. HEIC/HEIF files are tracked like other images; captioning converts them to JPEG before sending them to the configured vision provider. On macOS the default converter is sips; set CLAWGALLERY_HEIC_CONVERTER to an executable that accepts <input> <output> to use another converter.
Install / build
Release checklist
Before publishing a release, verify the package surface and the user-facing CLI:
cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo test --all-featurescargo package --allow-dirty --no-verify --listcargo package -p clawgallery-vdrcargo package -p clawgalleryafterclawgallery-vdris published or otherwise available to the registry resolvercargo run -- --helpcargo run -- vdr --helpcargo run -- daemon --help
Check that Cargo.toml includes the current version, repository, license, README, keywords, categories, and versioned workspace dependencies. Publish or verify clawgallery-vdr before verifying or publishing the root clawgallery crate, because the CLI package depends on the library crate by version. Re-read the Quickstart, command list, VDR setup, daemon notes, rename safety, auth, and state-file sections for consistency with CLI help before tagging.
First crates.io release
Trusted Publishing requires the crate to exist before the trusted publisher is configured. For the first release, publish manually with a crates.io API token:
Wait for clawgallery-vdr to appear in the crates.io index before publishing clawgallery; otherwise the root package cannot resolve its versioned library dependency.
After the first release, configure Trusted Publishing for both crates on crates.io with:
- repository:
NomaDamas/ClawGallery - workflow:
.github/workflows/publish.yml - environment:
crates-io
Future publishes should be created from a GitHub Release. The publish workflow authenticates with crates.io through GitHub OIDC, publishes clawgallery-vdr, waits for the registry index, then publishes clawgallery.
Quickstart
Semantic image search through local VDR with the packaged MLX daemon on macOS:
CLAWGALLERY_PYTHON="/mlx-embeddings/bin/python"
The MLX path uses mlx-embeddings with the late-interaction ColQwen2.5 model qnguyen3/colqwen2.5-v0.2-mlx. clawgallery vdr serve --backend mlx launches ClawGallery's packaged Python /embed daemon, so an installed Rust binary does not need the repository's scripts/ directory at runtime. The daemon binds to 127.0.0.1 by default and refuses non-loopback hosts unless --allow-remote is passed.
Legacy ColQwen2 server path (default VDR model: vidore/colqwen2-v1.0, dimensions 128):
Alternative Jina Omni embedding path:
Jina search must use the same model and dimensions as the synced VDR index. The Jina server enables Hugging Face trust_remote_code; if Hugging Face xet downloads stall on macOS, retry the first run with HF_HUB_DISABLE_XET=1.
Continuous polling:
--caption runs missing-caption generation after each ingest pass. --sync
then runs vdr sync, so poll --once --caption --sync performs one
bootstrap -> caption -> VDR sync cycle. Caption or VDR failures are written to
errors.jsonl and reported without stopping the poll loop.
State files
By default state is stored under ~/.config/clawgallery:
config.jsonfolders.jsonlimages.jsonlcaptions.jsonlrenames.jsonlerrors.jsonlvdr.sqlite3
Set CLAWGALLERY_CONFIG_DIR=/path/to/state to override this location.
State is split across three append-only JSONL event logs joined by image_id:
bootstrapwrites newImageRecords toimages.jsonl. Pass--pruneto also appendactive=falserecords for files that have disappeared from disk.captionwrites oneCaptionRecordper successful run tocaptions.jsonl.rename --applywrites aRenameRecordtorenames.jsonland appends a freshImageRecordwith the newpath(preserving the originalidandsha256).forget --file <path>appends anactive=falseImageRecordfor one tracked image; add--deleteto remove the disk file first.
Each downstream command (search, status, caption, rename) treats the latest record per path as authoritative and ignores active=false entries.
Visual model auth
ClawGallery supports multiple vision providers via a unified abstraction.
OpenAI-compatible (default)
Uses OpenAI-compatible /v1/responses requests for image understanding.
OPENAI_API_KEYOPENAI_BASE_URL(defaults tohttps://api.openai.com/v1)CLAWGALLERY_MODEL(defaults togpt-4.1-mini)
Best-effort Codex auth reuse is supported by reading $CODEX_HOME/auth.json or ~/.codex/auth.json for OPENAI_API_KEY or tokens.access_token.
Google Gemini
Uses the Gemini Generative Language API.
GEMINI_API_KEY- Default model:
gemini-2.5-flash(set via--modelor config)
Switching providers
Set the provider in config or override per-run:
Commands
clawgallery init
clawgallery folder add <path> [--recursive]
clawgallery folder remove <id-or-path>
clawgallery folder list
clawgallery bootstrap [--folder <id>] [--path <path>] [--prune]
clawgallery poll [--folder <id>] [--path <path>] [--once] [--interval <seconds>] [--prune] [--caption] [--sync] [--embedding-url <url>] [--vdr-model <model>] [--vdr-dimensions <n>] [--max-retries <n>]
clawgallery caption [--missing] [--file <path>] [--dry-run] [--model <model>] [--provider <provider>] [--concurrency <n>] [--max-retries <n>]
clawgallery rename [--apply] [--dry-run] [--file <path>] [--style title|caption|date-title] [--force]
clawgallery rename --undo [--last] [--file <path>] [--dry-run]
clawgallery forget --file <path> [--delete]
clawgallery dedup [--exact] [--similar] [--threshold <0..1>] [--json]
clawgallery search [--mode keyword|embedding] <query...> [--limit <n>] [--json] [--case-sensitive] [--no-fuzzy] [--embedding-url <url>]
clawgallery vdr sync [--prune] [--embedding-url <url>] [--model <model>] [--dimensions <n>] [--max-retries <n>]
clawgallery vdr serve [--backend mlx] [--host <host>] [--port <port>] [--model <model>] [--dimensions <n>] [--device auto|mps|cpu] [--python <path>] [--allow-remote]
clawgallery vdr status [--json]
clawgallery daemon install [--interval <seconds>] [--caption] [--sync] [--path <path>] [--folder <id>]
clawgallery daemon start|stop|status|uninstall|logs
clawgallery status
clawgallery skill path|print
Daemon
clawgallery daemon install writes a user service that runs clawgallery daemon run, which records a small status file and then starts the poll loop. macOS uses a LaunchAgent plist under ~/Library/LaunchAgents; Linux uses a systemd --user unit under the user config directory. Logs are written to daemon.log in the ClawGallery config directory, and daemon status reports the service file, log path, PID when known, and last start time. Set CLAWGALLERY_DAEMON_DIR to write the service file somewhere else for tests or managed deployments.
Dedup
clawgallery dedup reports duplicate candidates without deleting anything. With no mode flag it runs --exact, grouping active images that share the same sha256. Pass --similar to group active images whose VDR image embeddings meet --threshold (default 0.95); --exact and --similar are mutually exclusive so each report has one clear grouping mode. Run clawgallery vdr sync first so the local VDR index exists. --json emits one JSON object per group with a representative image and duplicate candidates including scores.
Search syntax
clawgallery search scans the local JSONL state on every invocation and ranks matches by weighted fields: title matches outrank description matches, which outrank path-only matches. The default text output includes the familiar path/title/caption lines plus score: and matches: lines. Agents and brittle scripts should prefer --json for JSONL records, or --no-fuzzy to preserve the old exact substring output format.
Pass --mode embedding to query the VDR index instead of the keyword matcher. Embedding search sends the query to the configured local embedding server, searches both image vectors and caption vectors, then returns the best matching vector per image. JSON output uses source: "embedding" and matched_field: "embedding_image" or matched_field: "embedding_caption".
Queries use nucleo/fzf-style operators:
| Syntax | Meaning | Example |
|---|---|---|
foo bar |
AND-match both atoms fuzzily | clawgallery search login error |
'foo |
Exact substring atom | clawgallery search "'github" |
^foo |
Prefix atom | clawgallery search ^Login |
foo$ |
Suffix atom | clawgallery search modal$ |
!foo |
Exclude substring | clawgallery search login !test |
!^foo, !foo$ |
Exclude prefix/suffix | clawgallery search !^Draft |
\ |
Literal space inside an atom | clawgallery search github\ actions |
Lowercase queries use smart-case matching; any uppercase atom becomes case-sensitive. Pass --case-sensitive to force case-sensitive matching. If the fuzzy pass returns no candidates, ClawGallery falls back to token/window typo tolerance for atoms of at least three characters. --no-fuzzy disables the DSL, fuzzy scoring, typo fallback, sorting, and score/matches output for compatibility with old scripts.
Visual Document Retrieval
VDR stores image embeddings for every active image and stores caption embeddings only when an active image has caption text. The store is embedded SQLite so it needs no daemon, works well on macOS, and stays inside the same config directory as the JSONL state. clawgallery vdr sync is incremental: unchanged image and caption content hashes are skipped, changed files or captions are re-embedded, and --prune deactivates vectors for images that are no longer active after bootstrap --prune.
The local embedding server contract accepts kind values image, text, or caption; caption is caption-document text encoded like text. For image inputs, value is the image path, including .heic/.heif paths, so the embedding server must have any needed HEIC decoder such as Pillow plus pillow-heif. role is document or query and a compatible server may ignore it. Responses may contain either one vector per input or multi-vector embeddings per input.
POST /embed
{"model":"vidore/colqwen2-v1.0","dimensions":128,"inputs":[{"kind":"image|text|caption","role":"document|query","value":"path or text"}]}
The packaged macOS-optimized server uses mlx-embeddings with qnguyen3/colqwen2.5-v0.2-mlx and 128-dimensional late-interaction ColQwen2.5 embeddings:
CLAWGALLERY_PYTHON="/mlx-embeddings/bin/python" \
The legacy local server uses vidore/colqwen2-v1.0 with 128-dimensional ColQwen2 embeddings:
The alternative Jina Omni path uses jinaai/jina-embeddings-v5-omni-small through sentence-transformers, enables Hugging Face remote model code, and uses 1024 dimensions. Pass matching --model jinaai/jina-embeddings-v5-omni-small --dimensions 1024 to clawgallery vdr sync when using it; embedding search should then query the same synced VDR index.
If Hugging Face xet downloads stall on macOS, retry the first run with HF_HUB_DISABLE_XET=1.
Set CLAWGALLERY_VDR_EMBEDDING_URL or pass --embedding-url to point the CLI at a different compatible local server.
Rename safety
Rename is dry-run by default. --apply is required to modify files. ClawGallery strips unsafe filename characters, preserves extensions, reserves suffix space for collisions, and refuses to overwrite existing files.
Dry-run rename and undo previews are side-effect free: they do not move files and do not append renames.jsonl records. Only applied renames and applied undo operations are recorded in rename history.
When rename --apply encounters a tracked path that no longer exists on disk (already renamed, deleted externally, etc.) it prints would skip (missing source) <path>, appends an active=false record so the entry stops following the live set, and continues with the rest of the batch. Per-image rename failures (collision, permission, IO) are logged to errors.jsonl and the run prints a final renamed N, skipped M meaningful-looking name(s), failed K summary instead of aborting on the first failure. API keys appearing in any error message (URL ?key=, Authorization: Bearer …, raw sk-… / AIza… strings) are redacted before being written to errors.jsonl or stderr.
Use rename --undo --last to reverse the latest applied rename, or add --file <path> to target one rename record. Undo reuses the same no-clobber file move, appends a reverse RenameRecord, marks the renamed path inactive, and appends the restored original ImageRecord. --dry-run previews the reverse move without touching files.
Meaningful-filename gate
rename skips files whose current name already looks human-meaningful and only renames stems that look auto-generated (IMG_0034, PXL_20240316_080000123, Screenshot 2025-11-01 at 14.32.55, 1696862563748, image (1), etc.). Classification runs in two tiers:
- A pure local regex covers ~12 well-known camera, screenshot, messenger, and download families plus pure numeric stems and copy/sequence suffixes. A regex match means
Genericand the stem is renamed without any model call. - Anything that does not match the regex is tagged
NeedsModel. Duringcaption, ClawGallery makes a separate text-only model call that sees only the filename stem (no image content) and asks whether the stem looks human-authored or auto-generated. The boolean is cached incaptions.jsonl(filename_meaningful: bool) so futurerenameruns reuse the answer.
Pass --force to rename every captioned image regardless of name, or --file <path> to rename a single explicit target without consulting the gate.
caption only announces metadata writes (captioned <path>); the gate decision lives in rename's output (dry-run X -> Y, would skip ..., renamed X -> Y). To audit the cached gate verdict for a specific image, read filename_meaningful from captions.jsonl.