clawgallery 0.2.2

Agent-native screenshot gallery CLI with JSONL metadata, visual captions, and safe rename workflows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
<div align="center">

<img src="assets/clawgallery-hero.jpg" alt="ClawGallery - a lobster curator sorting and inspecting artworks in a museum" width="100%">

# ClawGallery

**A friendly, agent-native gallery for all your screenshots and photos.**

</div>

---

## What is ClawGallery?

Your screenshots folder is a mess. Hundreds of `Screenshot 2025-11-01 at 14.32.55.png` and `IMG_0034.jpg` files, and no way to find the one you actually need.

ClawGallery is a small, fast command-line tool that turns that pile into a searchable gallery. Think of it as a tidy little lobster curator that:

- **Indexes** the image folders you point it at.
- **Understands** what's in each picture using a vision model, writing a title and caption for it.
- **Searches** by keyword *or* by visual meaning ("that login error screen") so you can find images even when the filename says nothing.
- **Renames** auto-generated filenames into human-readable ones — safely, with a dry-run by default.
- **Deduplicates**, finding exact and visually-similar copies.

Everything is stored as plain, append-only JSONL files on your own machine. No cloud, no database daemon, no lock-in. It's built to be driven by both humans and AI agents (every command speaks `--json`).

Supported formats: `png`, `jpg`, `jpeg`, `webp`, `avif`, `gif`, `heic`, `heif`. HEIC/HEIF images are converted to JPEG automatically before captioning (macOS uses `sips` by default; set `CLAWGALLERY_HEIC_CONVERTER` to use a different tool).

---

## Install

```bash
# Build, test, and install into your PATH
make ci
cargo install --path .
```

> **Enjoying ClawGallery? Please star the repo!** ⭐️
> It genuinely helps the project grow. If you have the GitHub CLI installed, it's one command:
>
> ```bash
> gh repo star NomaDamas/ClawGallery
> ```

---

## Quickstart

Get from zero to a searchable gallery in a few commands:

```bash
clawgallery init                    # set up local state
clawgallery folder add ~/Desktop    # tell it where your images live
clawgallery bootstrap               # scan the folders and index images
clawgallery search screenshot       # search by keyword right away
```

Want AI-generated titles and captions, then cleaner filenames? Preview first (everything below is a safe dry-run):

```bash
clawgallery caption --dry-run       # see what would be captioned
clawgallery rename --dry-run        # see the filename suggestions
```

When you're happy, drop `--dry-run` (and add `--apply` to actually rename files).

---

## Everyday usage

### 1. Point it at your images

```bash
clawgallery folder add ~/Pictures
clawgallery folder add ~/Pictures/screenshots --recursive
clawgallery folder list
clawgallery bootstrap               # add --prune to drop files deleted on disk
```

### 2. Caption your images

Captioning asks a vision model to describe each image, which powers better search and renaming. It can call a paid API, so **always preview first**:

```bash
clawgallery caption --dry-run
clawgallery caption --missing       # caption everything not yet captioned
clawgallery caption --file ~/Pictures/one.png
```

You'll need credentials for a provider (see [Vision model setup](#vision-model-setup)).

### 3. Search

By default, search is **hybrid**: Reciprocal Rank Fusion over keyword matches, V-SPLADE lexical ranks (if you synced `--backend vsplade`), and dense VDR ranks (if you built a visual index). Missing vector channels degrade instead of failing: keyword-only, keyword+dense, or keyword+sparse. Hybrid `--json` rows include `used_channels`, `skipped_channels`, and `degraded` so callers can tell a true hybrid result from a degraded one.

`--mode embedding` requires an active dense index; `--mode lexical` requires an active V-SPLADE index. Both fail before starting a query-embedding server and print the `vdr sync` command needed to build the missing index. `--mode keyword` always searches captions/paths and never needs a VDR server.

```bash
clawgallery search "login error"
clawgallery search "login error" --json --limit 5
clawgallery search --mode keyword "github actions"    # caption/path text only
clawgallery search --mode lexical "invoice total"     # V-SPLADE sparse retrieval
clawgallery search --mode embedding "sunset photo"    # dense visual only
clawgallery vdr status --json                         # dense vs sparse availability
```

Search understands fzf-style operators:

| Syntax | Meaning | Example |
|---|---|---|
| `foo bar` | Match both terms (fuzzy) | `clawgallery search login error` |
| `'foo` | Exact substring | `clawgallery search "'github"` |
| `^foo` | Starts with | `clawgallery search ^Login` |
| `foo$` | Ends with | `clawgallery search modal$` |
| `!foo` | Exclude | `clawgallery search login !test` |
| `\ ` | Literal space | `clawgallery search github\ actions` |

Lowercase queries are case-insensitive; add an uppercase letter (or `--case-sensitive`) to match case exactly. If nothing matches, ClawGallery automatically retries with typo tolerance. Use `--no-fuzzy` for plain exact-substring output.

### 4. Rename messy filenames

ClawGallery only renames files that *look* auto-generated (like `IMG_0034` or `Screenshot 2025-…`) and leaves your meaningful names alone. It never overwrites existing files and is **dry-run by default**:

```bash
clawgallery rename --dry-run        # preview
clawgallery rename --apply          # actually rename
clawgallery rename --undo --last    # undo the last applied rename
```

More on how it stays safe in [Rename safety](#rename-safety).

### 5. Find duplicates

`dedup` only *reports* — it never deletes anything:

```bash
clawgallery dedup                   # exact duplicates (same content)
clawgallery vdr sync                # build the visual index first
clawgallery dedup --similar --threshold 0.95 --json
```

To remove a duplicate you chose yourself: `clawgallery forget --file <path> --delete` (or omit `--delete` to just stop tracking it).

### 6. Keep it up to date automatically

Poll a folder for new images on an interval:

```bash
clawgallery poll --interval 30
clawgallery poll --interval 30 --caption --sync   # also caption + reindex each pass
```

`--caption` captions new images each pass; `--sync` then updates the visual index. Failures are logged to `errors.jsonl` and reported without stopping the loop.

Or run it as a background service (see [Run as a background service](#run-as-a-background-service)).

---

## Lexical search (V-SPLADE)

[V-SPLADE](https://github.com/NomaDamas/SPLADE-mlx) encodes each page image into a sparse vocabulary vector. Queries are an inference-free lookup, then ClawGallery ranks by sparse dot product. This is the `--mode lexical` backend and one of the hybrid RRF lists.

```bash
CLAWGALLERY_PYTHON=/path/to/splade-mlx/.venv/bin/python \
  clawgallery vdr sync --backend vsplade
clawgallery search --mode lexical "invoice total"
```

The managed V-SPLADE server requires `splade_mlx` and its MLX dependencies in the
Python interpreter used to start it. If `--python` and `CLAWGALLERY_PYTHON` are
not set, ClawGallery first uses an active `VIRTUAL_ENV` interpreter and then the
platform default (`python3` on macOS/Linux, `python` on Windows). It checks the
runtime before starting the server and prints the exact interpreter and
remediation when the dependency is missing. For example:

```bash
python3 -m pip install git+https://github.com/NomaDamas/SPLADE-mlx.git
CLAWGALLERY_PYTHON=python3 clawgallery vdr sync --backend vsplade
```

On Windows, install the same package into the active environment and use:

```powershell
python -m pip install git+https://github.com/NomaDamas/SPLADE-mlx.git
clawgallery vdr sync --backend vsplade
```

Default model: `NomaDamas/v-splade-efficient-mlx` (Apache-2.0, 50368-dim vocabulary). Sparse postings are stored in `vdr.sqlite3` next to dense VDR rows and do not deactivate them.

## Visual search (VDR)

"Visual Document Retrieval" is what lets ClawGallery find images by how they *look*, not just by their captions. It's optional — plain keyword search works without it — but it's what makes "find that screenshot of the error dialog" work even when the filename is garbage.

### How it works

ClawGallery stores image embeddings in an embedded SQLite file (`vdr.sqlite3`) right alongside your other state. No separate vector database, no extra daemon to babysit. Building the index is incremental: unchanged images and captions are skipped, and only new or changed content is re-embedded.

The embedding model itself runs in Python (the best late-interaction ColQwen-family runtimes on macOS are MLX/Python-based), but ClawGallery starts, waits for, and shuts down that runtime for you.

### Setup (macOS, recommended)

```bash
brew install rust uv
cargo install --path .

# Install the embedding runtime once
uv tool install mlx-embeddings --with pillow --with torch --with torchvision

# Build the visual index — ClawGallery starts the model server automatically
CLAWGALLERY_PYTHON="$(uv tool dir)/mlx-embeddings/bin/python" clawgallery vdr sync
```

Then just search — the query is embedded automatically:

```bash
clawgallery search "login error"              # hybrid RRF (keyword + lexical + visual)
clawgallery search --mode lexical "invoice"   # V-SPLADE sparse retrieval
clawgallery search --mode embedding "sunset"  # visual only
clawgallery vdr status --json
```

The default model is `qnguyen3/colqwen2.5-v0.2-mlx` (128 dimensions). The first run downloads and caches model weights. If Hugging Face downloads stall on macOS, retry with `HF_HUB_DISABLE_XET=1`.

### Jina v5 Omni retrieval on Apple Silicon

ClawGallery also packages the MLX conversion of `jinaai/jina-embeddings-v5-omni-small-retrieval-mlx` (1024 dimensions). It requires Apple Silicon and loads the immutable Hugging Face revision `049ae923674456656be891ebb22849dd58124994`.

```bash
uv venv ~/.local/share/clawgallery/jina-mlx
uv pip install --python ~/.local/share/clawgallery/jina-mlx/bin/python \
  'mlx>=0.23' tokenizers huggingface_hub 'transformers>=4.57,<5' pillow \
  torch torchvision requests librosa av

CLAWGALLERY_PYTHON=~/.local/share/clawgallery/jina-mlx/bin/python \
  clawgallery vdr sync --backend jina-mlx
```

The first sync downloads and caches the model weights. Later visual searches read the model ID and dimensions from the active index, so they automatically start the Jina MLX runtime without repeating `--backend`. Keep `CLAWGALLERY_PYTHON` pointed at the Jina environment when searching.

This Jina model is licensed under CC BY-NC 4.0 and is restricted to noncommercial use. Review the model license before using it in a product or service.

### Using your own embedding server

To reuse a long-running server instead of the managed one, point ClawGallery at it and it won't auto-start anything:

```bash
# Terminal A: keep a server running
CLAWGALLERY_PYTHON="$(uv tool dir)/mlx-embeddings/bin/python" \
  clawgallery vdr serve --backend mlx --host 127.0.0.1 --port 8765

# Terminal B: sync and search against it
clawgallery vdr sync --embedding-url http://127.0.0.1:8765
clawgallery search --mode embedding "login error" --json
```

You can also set `CLAWGALLERY_VDR_EMBEDDING_URL` instead of passing `--embedding-url`. The managed server binds to `127.0.0.1` and refuses non-loopback hosts unless you pass `--allow-remote`.

<details>
<summary>External embedding backends (legacy ColQwen2 and SentenceTransformer Jina Omni)</summary>

**Legacy ColQwen2** (`vidore/colqwen2-v1.0`, 128 dims):

```bash
uv pip install colpali-engine torch pillow
python scripts/colqwen2_server.py --device auto
clawgallery vdr sync --no-auto-start --model vidore/colqwen2-v1.0 --dimensions 128
clawgallery search --mode embedding "login error" --json
```

**SentenceTransformer Jina Omni** (`jinaai/jina-embeddings-v5-omni-small`, 1024 dims):

```bash
python scripts/jina_omni_server.py --device auto
clawgallery vdr sync --no-auto-start --model jinaai/jina-embeddings-v5-omni-small --dimensions 1024
clawgallery search --mode embedding "login error" --json
```

This external SentenceTransformer path is separate from the managed `jina-mlx` backend above. Search must use the same model and dimensions as the synced index. The server enables Hugging Face `trust_remote_code`; if xet downloads stall, retry with `HF_HUB_DISABLE_XET=1`.

The embedding server contract is a single `POST /embed`:

```text
{"model":"vidore/colqwen2-v1.0","dimensions":128,"inputs":[{"kind":"image|text|caption","role":"document|query","value":"path or text"}]}
```

`kind` is `image` (path), `text`, or `caption`. For images, `value` is the file path (including `.heic`/`.heif`), so the server needs an HEIC decoder such as Pillow + `pillow-heif`.

</details>

---

## Vision model setup

Captioning needs a vision-capable model. ClawGallery supports two providers.

### OpenAI-compatible (default)

Uses `/v1/responses`-style requests.

- `OPENAI_API_KEY` — your key
- `OPENAI_BASE_URL` — defaults to `https://api.openai.com/v1`
- `CLAWGALLERY_MODEL` — defaults to `gpt-4.1-mini`

It can also reuse Codex credentials from `$CODEX_HOME/auth.json` or `~/.codex/auth.json`.

### Google Gemini

- `GEMINI_API_KEY` — your key
- Default model: `gemini-2.5-flash`

### Choosing a provider

```bash
clawgallery caption --provider gemini --model gemini-2.5-flash
clawgallery caption --provider openai-compatible --model gpt-4.1-mini
```

---

## How your data is stored

Everything lives in `~/.config/clawgallery` by default (override with `CLAWGALLERY_CONFIG_DIR`):

| File | What it holds |
|---|---|
| `config.json` | Your settings |
| `folders.jsonl` | Registered folders |
| `images.jsonl` | One record per discovered / pruned / renamed image |
| `captions.jsonl` | One record per successful caption |
| `renames.jsonl` | Rename history |
| `errors.jsonl` | Logged failures (API keys redacted) |
| `vdr.sqlite3` | The visual embedding index |

The event logs are **append-only** and joined by `image_id`. This is deliberate: cheap, free, repeatable indexing (`bootstrap`) is kept separate from paid network calls (`caption`) and from irreversible file changes (`rename --apply`). Every command treats the newest record per file as the truth and ignores anything marked inactive.

---

## Rename safety

Renaming files is the one thing that touches your disk, so it's cautious by design:

- **Dry-run by default.** You must pass `--apply` to move files. Dry-runs never touch files or write history.
- **Meaningful names are left alone.** Only auto-generated stems get renamed (`IMG_0034`, `PXL_20240316_080000123`, `Screenshot 2025-11-01 at 14.32.55`, `1696862563748`, `image (1)`, …). A local regex catches ~12 common camera/screenshot/messenger families for free; anything ambiguous triggers a text-only model check on the *filename* (no image content) whose answer is cached as `filename_meaningful` in `captions.jsonl`.
- **No clobbering.** Unsafe characters are stripped, extensions preserved, and existing files are never overwritten.
- **Batch-safe.** If a tracked file has vanished from disk, ClawGallery marks it inactive and keeps going. Per-file failures are logged and summarized (`renamed N, skipped M, failed K`) instead of aborting.

```bash
clawgallery rename --dry-run                    # preview the whole batch
clawgallery rename --apply                       # apply
clawgallery rename --apply --file one.png        # single file, skips the gate
clawgallery rename --apply --force               # rename everything captioned
clawgallery rename --undo --last                 # reverse the last apply
```

---

## Run as a background service

Install a user service that polls for new images continuously:

```bash
clawgallery daemon install --interval 30 --caption --sync
clawgallery daemon start
clawgallery daemon status
clawgallery daemon logs
clawgallery daemon stop
clawgallery daemon uninstall
```

On macOS this is a LaunchAgent (`~/Library/LaunchAgents`); on Linux it's a `systemd --user` unit. Logs go to `daemon.log` in your config directory. Set `CLAWGALLERY_DAEMON_DIR` to write the service file elsewhere.

---

## Command reference

```text
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|lexical|hybrid] <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>] [--auto-start|--no-auto-start] [--backend mlx|jina-mlx|vsplade] [--host <host>] [--port <port>] [--device auto|mps|cpu] [--python <path>] [--allow-remote]
clawgallery vdr serve [--backend mlx|jina-mlx|vsplade] [--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
```

`jina-mlx` supports `--device auto|mps`; `cpu` is available only with the
default `mlx` backend.

---

## For AI agents

ClawGallery ships a skill so agents can drive it safely. Every command supports `--json` for stable, parseable output — prefer it. Run `clawgallery skill print` to load the guidance, and remember the safe defaults: `rename` never touches files without `--apply`, and bulk `caption --missing` may cost money, so preview with `--dry-run` first.

---

## Community

- Contributing: [CONTRIBUTING.md]CONTRIBUTING.md
- Code of conduct: [CODE_OF_CONDUCT.md]CODE_OF_CONDUCT.md
- Security: [SECURITY.md]SECURITY.md
- Changelog: [CHANGELOG.md]CHANGELOG.md

## License

Apache-2.0. See [LICENSE](LICENSE).

If ClawGallery saved you from filename chaos, don't forget to **⭐️ star the repo** — `gh repo star NomaDamas/ClawGallery`. Thank you!