Carrel in ten minutes¶
A guided tour of the desk. Every command and output below was actually run against copies of this repo's test fixtures (and, for §6–7, a small scratch docs folder); your hashes, timestamps and absolute paths will differ, the shapes won't.
Prerequisite: carrel installed and on PATH (INSTALL.md) — the
tour assumes uv tool install 'carrel[all]', so the TUI, xlsx reading and
exact token counts all work. Working from the repo instead? Substitute
uv run carrel everywhere.
Related docs: Reference · Configuration · Troubleshooting · Cookbook · README
0. Check the room — doctor¶
carrel doctor
carrel 0.5.0 · python 3.12.13
external tools
│ pandoc │ found │ pandoc 3.7.0.2 │
│ tesseract │ found │ tesseract 5.5.0 │
│ piper │ MISSING │ pipx install piper-tts │
│ git │ found │ git version 2.53.0 │
…
command capabilities
│ catalog │ ok │ desk db export/import/status (stdlib sqlite) │
│ convert │ ok │ built-in md→html fallback; pandoc widens formats… │
│ ocr │ ok │ tesseract for images, ocrmypdf for PDF text layers │
…
tesseract languages: eng, osd
Every MISSING row shows the exact install command. Missing tools never
crash carrel — a command that needs one exits with code 3 and that same hint.
The same goes for the optional Python extras: on a plain install carrel desk
exits 3 with uv tool install 'carrel[tui]'.
1. Set up a playground¶
From a clone of this repo, copy the committed test fixtures somewhere
disposable (the glob picks up sample.docx, sample.xlsx, sample.epub…
alongside the older ones):
mkdir ~/carrel-tour && cp tests/fixtures/sample.* tests/fixtures/*.pdf ~/carrel-tour/
cd ~/carrel-tour
2. Look at a file — inspect¶
carrel inspect text+image.pdf
name text+image.pdf
path text+image.pdf
type pdf
mime application/pdf
size 51379
sha256 cc62bbfc72b96e74c5434f42871ce226759fc5da321029720240c3fa2477f4fb
detail:
encrypted False
pages 2
producer ReportLab PDF Library - (opensource)
form_fields 0
annotations 0
Detail is per-type — a CSV reports its dialect and columns, a spreadsheet its sheets:
carrel inspect sample.xlsx
name sample.xlsx
type xlsx
mime application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
size 5587
sha256 57a325efe344106367fb03df213a021e73e6a6543074b34a98b95d1e478f9d82
detail:
sheets [{"name": "Books", "rows": 4, "cols": 3}, {"name": "Loans", "rows": 4, "cols": 3}]
Add the global --json flag and you get one clean JSON object, ready for
jq:
carrel --json inspect sample.csv
{
"path": "sample.csv",
"type": "csv",
"sha256": "233467d1df2da52c66a8fc826ca26d65d03cc6b3ba654d9d49a8bfceb32294f8",
"detail": {
"delimiter": ",",
"columns": ["id", "title", "shelf", "year", "checked_out"],
"column_count": 5,
"rows": 20
}
}
(Trimmed — the real object also carries name, size, mtime, mime.)
3. Change a file's shape — convert¶
carrel convert sample.md --to pdf
carrel convert sample.csv --to md -o catalog.md
carrel convert sample.docx --to md --force # office/ebook formats read through pandoc
carrel convert sample.xlsx --to csv --sheet 2 # xlsx → csv/json (needs the office extra)
sample.md -> sample.pdf [pandoc+weasyprint]
sample.csv -> catalog.md [builtin]
sample.docx -> sample.md [pandoc]
sample.xlsx -> sample.csv [openpyxl]
The bracket names the tool chain that did the work. Outputs are never
overwritten silently — repeat a conversion and you get
error: refusing to overwrite sample.pdf (use --force) (which is why the docx
line above, landing on an existing sample.md, passes --force). docx, odt,
epub and rtf read via pandoc; md/html/txt write to docx and odt; docx and epub
round-trip. Run carrel convert --help to see the full SRC → target matrix.
4. Bundle files for an LLM — pack¶
carrel pack . --include '*.md' --include '*.csv' -o context.md --stats
pack stats
┃ path ┃ type ┃ size ┃ tokens_est ┃
│ catalog.md │ md │ 873 B │ 243 │
│ sample.csv │ csv │ 589 B │ 187 │
│ sample.md │ md │ 656 B │ 183 │
│ TOTAL │ 3 in / 0 skip │ 2.1 KB │ 613 │
wrote context.md
context.md opens with a header and file tree, then one fenced section per
file — paste-ready context. It honors .gitignore (including ! negation),
budgets with --max-bytes/--chunk, and emits --format xml
(Claude-friendly) or json. tokens_est is a chars/3.6 estimate;
--tokenizer exact counts with tiktoken and relabels the column tokens
(needs the tokens extra). --outline shows structure instead of contents
(.py defs/classes with line numbers, .md headings); --dedupe-content
inlines identical files once.
5. Build a search index — index + search¶
carrel index
indexing catalog.md
indexing sample.csv
…
│ indexed 15 │ skipped 0 │ pruned 0 │ errors 0 │
That creates .carrel/carrel.db in the current directory (the "desk root" —
control it with the global --root, see CONFIGURATION.md).
Now full-text search it:
carrel search "shelf" --limit 3
1. catalog.md (score -0.98)
| id | title | [shelf] | year | checked_out | | 1 | Palimpsest Vol 1 | B2 …
2. sample.csv (score -0.98)
id, title, [shelf], year, checked_out …
FTS5 syntax works ("exact phrase", term1 AND term2), matches are
bracketed in the snippet, and --json gives [{path, score, snippet}].
Images and scanned PDFs get searchable text too if you index with --ocr.
carrel index --status reports what has changed on disk since the last run
(see §7).
6. Pack what matters — pack --query¶
The index is also how pack decides what is relevant. Take a small docs
folder — two guides, two reference pages, a meeting note, a CSV, and a stray
scratch.py:
carrel --root docs index
carrel --root docs pack docs --query release --stats
indexing guides/onboarding.md
indexing guides/release-checklist.md
indexing notes/meeting-2026-09-01.txt
indexing notes/topics.csv
indexing reference/exit-codes.md
indexing reference/glossary.md
│ indexed 6 │ skipped 0 │ pruned 0 │ errors 0 │
pack stats
┃ path ┃ type ┃ size ┃ tokens_est ┃ score ┃ note ┃
│ guides/release-checklist.md │ md │ 202 B │ 56 │ -0.000 │ │
│ notes/topics.csv │ csv │ 48 B │ 15 │ -0.000 │ │
│ reference/glossary.md │ md │ 105 B │ 29 │ -0.000 │ │
│ notes/meeting-2026-09-01.txt │ txt │ 154 B │ 43 │ -0.000 │ │
│ guides/onboarding.md │ md │ 152 B │ 43 │ -0.000 │ │
│ TOTAL │ 5 in / 0 skip │ 661 B │ 186 │ │ │
Five of six indexed files mention "release"; exit-codes.md does not and is
left out. Rows are in relevance order, not tree order, and the pack header
records the query:
carrel --root docs pack docs --query release --top 5 -o ctx.md
wrote ctx.md (5 files, ~186 tokens_est)
# carrel pack
- generated-by: carrel 0.5.0
- root: …/docs
- files: 5 included, 0 skipped
- tokens_est: 186
- query: 'release' (top 5, 5 hit(s))
Two honest notes. First, bm25 scores from FTS5 are tiny on small documents,
so the human table prints -0.000; --json carries the real value
(-1.5e-06 here) along with meta.query, meta.hits and meta.top.
Second, --query only ranks files the index knows about — but that now
includes source and config files (.py, .toml, .yaml, …), indexed as type
code, so scratch.py ranks for "release" alongside the documents. Filter to
them with search --type code, or index documents only with --no-source.
The walk honors .gitignore, so node_modules/ and build/ stay out.
In scripts, a query with no hits is exit 5 — the default under --json, and
--fail-empty in human mode:
$ carrel --root docs pack docs --query xyzzyplugh --fail-empty --tree-only
error: packed no files: no document contains every term of --query 'xyzzyplugh'
(FTS5 requires all of them; try fewer terms, or OR between them)
$ echo $?
5
Inside a repository, --since REF and --changed pack what changed
instead (via the git adapter):
carrel pack . --since HEAD~1 --tree-only
- files: 2 included, 0 skipped
- since: HEAD~1 (2 changed, 0 removed)
The whole §6 flow is cookbook recipe 10; run
bash examples/cookbook/10-pack-what-matters.sh from a checkout.
7. Carry your tags and notes — catalog¶
Tags and notes are the one thing the desk cannot rebuild from your files, so
they export as a small JSON document. Working in the docs folder from §6:
carrel tag add guides/release-checklist.md release process
carrel tag add reference/glossary.md reference
carrel note add guides/release-checklist.md "Step 4 needs the PyPI trusted publisher set up first."
carrel catalog status
guides/release-checklist.md: process, release
reference/glossary.md: reference
note 1 on guides/release-checklist.md @ 2026-09-11T13:34:25
/home/you/docs/.carrel/carrel.db (schema 2)
desk catalog
┃ files ┃ docs ┃ tags ┃ notes ┃ meta ┃ changed ┃ missing ┃ unindexed ┃
│ 6 │ 6 │ 3 │ 1 │ 0 │ 0 │ 0 │ 0 │
(Relative paths for tag/note resolve against your current directory, not
--root, so run them from inside the desk root or pass absolute paths.)
carrel catalog export -o desk.json
wrote /home/you/docs/desk.json: 2 file(s), 3 tag(s), 1 note(s), 0 field(s)
(field(s) counts carrel meta fields, which travel with the catalog too —
none have been set here.)
desk.json is deterministic (sorted by path; only exported changes between
runs), so it diffs cleanly and can live next to the files in version control:
{
"schema": 2,
"product": "carrel",
"version": "0.5.0",
"exported": "2026-09-11T17:34:25+00:00",
"root": "/home/you/docs",
"files": [
{
"path": "guides/release-checklist.md",
"tags": [
"process",
"release"
],
"notes": [
{
"created": 1789148065.4799895,
"body": "Step 4 needs the PyPI trusted publisher set up first."
}
],
"meta": []
},
{
"path": "reference/glossary.md",
"tags": [
"reference"
],
"notes": [],
"meta": []
}
]
}
Move the folder, rebuild the index, merge the catalog back in — importing the same document twice adds nothing:
$ rm -rf .carrel && carrel index --json
{
"indexed": 7,
"skipped": 0,
"pruned": 0,
"errors": []
}
$ carrel catalog import desk.json
imported 3 tag(s), 1 note(s), 0 field(s) across 2 file(s)
$ carrel --json catalog import desk.json
{
"tags_added": 0,
"notes_added": 0,
"meta_set": 0,
"files_touched": 0,
"skipped_missing": 0,
"tags_removed": 0,
"notes_removed": 0,
"meta_removed": 0,
"skipped_outside": 0
}
(Seven, not six: desk.json now sits in the folder and is a JSON document
like any other, so the rebuilt index picks it up. skipped_outside counts
catalog entries whose path falls outside the desk root — check it in scripts,
since those are dropped silently.)
catalog status (or index --status) is also how you learn the index is
stale — edit one file, delete another, and:
┃ files ┃ docs ┃ tags ┃ notes ┃ meta ┃ changed ┃ missing ┃ unindexed ┃
│ 7 │ 7 │ 3 │ 1 │ 0 │ 1 │ 1 │ 0 │
changed reference/exit-codes.md
missing notes/topics.csv
hint: `carrel index` refreshes changed/unindexed files; `carrel index --prune` drops missing ones
8. Make thumbnails — thumb¶
carrel thumb text+image.pdf sample.jpg --size 200
text+image.pdf -> thumbs/text+image.png (155x200)
sample.jpg -> thumbs/sample.png (200x150)
PDFs are rasterized at page one; aspect ratio is always preserved.
9. Automate a folder — watch¶
watch runs shell actions on file events, with {path}, {name}, {dir}
substituted. Try a self-terminating example (--once exits after the first
action, --timeout is a safety net):
mkdir -p inbox
carrel watch inbox --glob '*.jpg' --run 'echo saw {name}' --once --timeout 60 &
cp sample.jpg inbox/drop.jpg
watching /home/you/carrel-tour/inbox (on: created, modified, glob: *.jpg) — Ctrl-C to stop
[modified] …/inbox/drop.jpg :: echo saw drop.jpg -> rc=0
saw drop.jpg
Real-world version (thumbnail every PDF that lands in an inbox — note the output goes outside the watched directory so it can't re-trigger):
carrel watch inbox --glob '*.pdf' --run 'carrel thumb {path} --out-dir thumbs'
WSL2 note: watch a directory under ~/… (ext4), not /mnt/c/… — see
Troubleshooting.
10. Sit down at the desk — desk¶
Everything above, interactively:
carrel desk
A three-pane TUI: directory tree · inspector (metadata, text preview,
tags/notes) · actions (convert / OCR / thumbnail / pack / index / tag / note).
Action outputs land in ./carrel-out/. Keys: q quit, / search, t tag,
n note. It needs the tui extra; without it:
$ carrel desk
error: textual is not installed (optional extra 'tui') — run: uv tool install 'carrel[tui]' (from a checkout: uv sync --extra tui)
11. Stop typing flags — completion¶
carrel completion bash --install-hint | tail -4
The script itself is generated in-process from the real command tree, so it always matches the installed version. Enable it once per shell:
eval "$(carrel completion bash)" # ~/.bashrc
eval "$(carrel completion zsh)" # ~/.zshrc, after compinit
carrel completion fish > ~/.config/fish/completions/carrel.fish # fish autoloads it
Any other shell name exits 2; --json gives {"shell": …, "script": …}.
Details per shell in INSTALL.md.
Where to next¶
- Full flag-by-flag docs for all 33 commands: REFERENCE.md — including OCR, dedupe, organize, redact, sign, audiobook, and the MCP server (fourteen tools, two resource templates — see AGENTS.md).
- Runnable recipes: COOKBOOK.md (
examples/cookbook/andsnippets/in the repo). - Claude Code integration: the repo doubles as a plugin marketplace
(
/inspect,/pack,/watch-folder, …) — see MARKETPLACE.md.