FEATURES — capability × strategy matrix¶
Strategies: wrap:<tool> (external binary via adapter) · lib:<pypi> · custom (pure Python) · degrade-if-missing · stretch.
Tiers: MVP (must ship before flagship), v1 (shipped in v0.1.0 after MVP), v0.2.0 (shipped on feat/v0.2.0, specs 15–21), stretch (attempted last / cut candidates).
File types: pdf md jpg jpeg png ico txt html json xml csv docx odt epub rtf xlsx (xlsm) eml mbox (mbx).
| Capability | Command | Strategy | Types | Tier |
|---|---|---|---|---|
| File conversion | carrel convert |
wrap:pandoc (md/html/txt + docx/odt/epub/rtf read and write), wrap:weasyprint (html→pdf), wrap:poppler pdftotext (pdf→txt/md), lib:Pillow + wrap:imagemagick (jpg/png/ico), custom (json↔csv↔xml via stdlib), lib:openpyxl via carrel[office] (xlsx→csv/json) |
all | MVP (+ office in v0.2.0) |
| Text recognition (OCR) | carrel ocr |
wrap:ocrmypdf (pdf), wrap:tesseract (images) | pdf jpg jpeg png | MVP |
| Diff / compare | carrel diff |
custom (text unified diff, structural JSON/CSV diff), wrap:pdftotext (pdf text diff), lib:Pillow (image pixel/percent diff) | all | MVP |
| General editing | carrel edit |
wrap:qpdf + lib:pypdf (pdf merge/split/rotate/extract-pages), lib:Pillow (image resize/rotate/crop/strip), custom (text/md find-replace, json set/del via dotted path) | all | MVP |
| Context engineering | carrel pack |
custom (tree+content dump, include/exclude globs, ignore-file aware, chunking, char/token estimates; md/xml/json output formats) | all text-ish; binaries summarized via inspect | MVP |
| Query-driven & version-control-aware packing | carrel pack --query / --top / --since / --changed / --dedupe-content / --tokenizer exact / --outline |
custom over FTS5 ranking (relevance order + per-file score); wrap:git for changed-file lists; !pattern negation in ignore files; lib:tiktoken (o200k_base) via carrel[tokens]; ast-based outline for .py, heading outline for .md |
all indexed types (--query); all (--since, --outline) |
v0.2.0 |
| Object/metadata inspection | carrel inspect |
wrap:exiftool (degrade→lib:Pillow EXIF + lib:pypdf metadata), custom (magic bytes, structure summaries for json/xml/csv/html, docx core props, epub OPF, xlsx sheets) | all | MVP (+ office in v0.2.0) |
| Indexing & search | carrel index / carrel search |
custom (SQLite FTS5; per-type text extractors reuse convert/ocr paths); index --status reports stale rows |
all | MVP |
| Catalog: schema migrations, export/import, status | carrel catalog export/import/status, carrel index --status |
custom (PRAGMA user_version + ordered MIGRATIONS; deterministic JSON document of tags + notes; merge or --replace) |
— | v0.2.0 |
| Thumbnails | carrel thumb |
wrap:pdftoppm (pdf), lib:Pillow (images), wrap:imagemagick fallback; html thumb = render pdf→ppm | pdf + images (+html via pdf) | MVP |
| Folder watch | carrel watch (--recursive --existing --stable --poll --done-dir --error-dir --log --print-service) |
lib:watchdog (inotify, or PollingObserver with --poll for /mnt/c and shares), custom rule→action mapping via core/actions.py; settle-wait, filing away, JSON log, systemd/schtasks service printer |
all | MVP (+ v0.4.0 flags) |
| Batch runner | carrel batch (--jobs --manifest --resume --dry-run --fail-fast --json-lines) |
custom (core/actions.py shell actions per file, ThreadPool parallelism, JSON-lines manifest; walks like index) |
all | v0.4.0 |
| Field extraction | carrel fields (--profile --date-order --set --save) |
custom (core/money.py + core/dates.py + label heuristics over textextract; core/patterns.py for invoice/PO/IBAN/account; confidence per field) |
all text-bearing | v0.4.0 |
| Inbox intake | carrel intake INBOX --to DEST (--apply --watch --by ym\|period --fiscal-start --template --tag) |
custom pipeline over fields + refs + rename + fsops + index + meta + tag; scans OCRed into a searchable copy with the original kept under _originals/ (D-014); dry-run default |
all | v0.4.0 |
| Template rename | carrel rename --template '{date}_{vendor}_{ref}{ext}' |
custom (fields/meta/refs placeholders, slugified, collision-safe; desk rows follow via core/fsops.py, which organize --apply now uses too) |
all | v0.4.0 |
| Doctor / env probe | carrel doctor |
custom (re-probes adapters, prints capability table + apt hints; shows via CARREL_BIN_* for pinned binaries and the extra to install for gated commands) |
— | MVP |
| Whole desk over MCP | carrel mcp |
custom stdlib JSON-RPC 2.0 on stdio: 14 tools (search, pack, inspect, tag, note, index, convert, diff, redact, doctor, meta, fields, mail, refs) delegating to the command impl functions + resource templates carrel://file/{path}, carrel://search/{query} |
all | v1 (3 tools) → v0.2.0 (10 tools + resources) → v0.4.0 (14) |
| Install ergonomics | carrel completion bash/zsh/fish; extras tui/office/tokens/all; CARREL_BIN_<NAME> override; git adapter |
lib:click completions (in-process); pyproject extras (D-007); env-var override (D-008) | — | v0.2.0 |
| Dedupe | carrel dedupe |
custom (BLAKE2 content hash groups; --near perceptual dHash for images, custom impl, no numpy) |
all | v1 |
| File/folder organization | carrel organize |
custom (rules: by type/date/exif-date; dry-run default) | all | v1 |
| Audiobook (TTS) | carrel audiobook |
wrap:espeak-ng → wrap:ffmpeg (mp3/ogg, chapters from md headings); adapter prefers piper/edge-tts if present; documents narrate via textextract (pandoc) |
txt md pdf docx odt epub rtf | v1 (+ documents in v0.2.0) |
| Redaction | carrel redact |
custom (pattern/regex redaction for txt/md/html/json/csv/xml with the built-in kinds from core/patterns.py — PII plus invoice/po/iban/routing/…; label-driven kinds keep the label); pdf: true redaction by rasterize→blackbox→rebuild (text layer destroyed by design; documented) |
text types + pdf | v1 |
| Signatures | carrel sign |
lib:pypdf+lib:reportlab (visible PDF stamp), custom (sha256 MANIFEST + wrap:gpg detached sig, verify mode) | pdf + any (manifest) | v1 |
| Notes/comments (annotations) | carrel note |
lib:pypdf (PDF text annotations, list/add), custom (sidecar notes in index DB for any file) | pdf + all (sidecar) | v1 |
| Tagging | carrel tag |
custom (tags in index DB; add/rm/ls/find) | all | v1 |
| Typed metadata fields | carrel meta set/get/ls/rm/find/export, search --meta |
custom (schema v2 meta table; kinds str/num/date/bool inferred, canonical storage; typed find conditions = != > >= < <= ~ ?; CSV/JSON export; travels with catalog) |
all | v0.4.0 |
| Email files | .eml/.mbox everywhere: inspect, convert (eml → md/txt/html/pdf, mbox → md/txt), index/search --type eml, pack, diff, organize → mail/, refs, the Read guard |
custom (stdlib email + mailbox via core/mail.py; shape sniff only for unmapped extensions, D-012) |
eml mbox | v0.4.0 |
| Mail tools | carrel mail attachments/split/threads/pst |
custom (attachments with sha256, mbox → dated .eml files, Message-ID/In-Reply-To/References threads); wrap:readpst (pst-utils) for Outlook .pst/.ost (-e one .eml per message, -r one mbox per folder) |
eml mbox pst | v0.4.0 |
| Reference numbers | carrel refs (--tag, --link, --pattern NAME=REGEX) |
custom (core/patterns.py: label-driven invoice/po/order/check/account/tracking/ticket + iban/routing/ein/vat/isbn/gtin/doi/ups/usps with check-digit validators; text via textextract; ref:<kind>:<value> tags cross-link files) |
all | v0.4.0 |
| Form building | carrel form |
custom (JSON spec → HTML form; → PDF form via weasyprint for print-fill), lib:pypdf (fill existing AcroForm PDF, list fields) | html pdf json | v1 |
| Image extraction | carrel extract-images |
wrap:pdfimages (pdf), wrap:icotool (ico frames), custom (html <img> local refs) |
pdf ico html | v1 |
| Soft proofing (ICC) | carrel proof |
wrap:imagemagick -profile with system ICC profiles (probed present); reports ΔE summary via Pillow |
jpg jpeg png pdf(raster) | v1 |
| Color management | carrel color |
lib:Pillow+ImageCms (profile convert/assign, palette extraction, contrast check) | images | v1 |
| Agent workflows & loops | marketplace plugin carrel-agent |
custom (slash commands + agents + a watch-loop skill that pairs carrel watch with claude -p) |
— | v1 |
| Drift gates & CI matrix | scripts/sync_reference.py (generated docs/REFERENCE.md, --check in CI); docs/COOKBOOK.md pulls every recipe/snippet in at build time; Linux 3.12–3.14 with coverage floor, test-minimal (macos) required, test-minimal (windows) advisory |
custom | — | v0.2.0 |
Plugin surface generated from --help |
7 plugins (carrel-inspect, carrel-convert, carrel-organize, carrel-documents, carrel-watch, carrel-agent, carrel-guard) |
custom; every command a slash command, usage blocks generated by scripts/sync_plugins.py --check; hooks PreToolUse(Read) → text conversion, SessionStart → capability summary, PostToolUse(Write\|Edit) → reindex |
— | v0.2.0 |
| Source-tree indexing | carrel index (--no-source, --no-gitignore) |
custom (FileType.CODE from SOURCE_EXTENSIONS; .gitignore walk shared with pack via core/ignore.py) |
source + config types | post-v0.2.0 |
| Invented: desk TUI | carrel desk |
lib:textual via carrel[tui] (flagship; drives core library) |
all | v1 (flagship) |
| Invented: recipes | carrel run <recipe.yaml> |
custom mini-pipeline runner | — | stretch |
| Cryptographic PDF signing (PAdES) | — | stretch (needs pyHanko + key mgmt) | stretch |
Office & ebook formats (spec 18, shipped)¶
Detection is by bytes ({\rtf prefix; zip containers probed for mimetype / [Content_Types].xml), so a mis-named file is still handled — carrel inspect mystery.bin on a renamed docx reports type: docx. One core.textextract branch feeds index, search, pack, diff and audiobook for every row. Word-processor and ebook reads/writes go through pandoc (degrade-if-missing → exit 3 with sudo apt install pandoc); xlsx reads use openpyxl from the office extra (uv tool install 'carrel[office]'; exit 3 with that hint when absent). Spreadsheet writing is out of scope for this release.
| Format | Detect | Text (extract_text) |
convert from |
convert to |
inspect detail |
|---|---|---|---|---|---|
| docx | .docx; zip with [Content_Types].xml + word/ |
wrap:pandoc -t plain |
md html txt pdf (pandoc → weasyprint) epub | from md html txt (pandoc), from epub | paragraphs, words, title/author/created (docProps/core.xml) |
| odt | .odt; zip mimetype = application/vnd.oasis.opendocument.text |
wrap:pandoc | md html txt pdf | from md html txt | words |
| epub | .epub; zip mimetype = application/epub+zip |
wrap:pandoc | md html txt pdf docx | from docx | title, creator, language, spine_items (OPF), words |
| rtf | .rtf; {\rtf header |
wrap:pandoc | md html txt pdf | — | words |
| xlsx | .xlsx .xlsm; zip with [Content_Types].xml + xl/ |
lib:openpyxl (# sheet heading + CSV rows) |
csv (--sheet NAME\|N\|all) json ({sheet: [row objects]}) |
— | sheets: [{name, rows, cols}] |
Proof (executed 2026-09-04, TEST_REPORT.md): sample.md → docx → md keeps the fixture sentinel melodious cartography; sample.xlsx --to csv --sheet 2 yields the Loans sheet; inspect sample.xlsx lists both sheets with rows: 4, cols: 3.
Email (spec 28, shipped)¶
One core.textextract branch (core/mail.py, stdlib only) feeds every command. Detection is by extension (.eml, .mbox, .mbx) or, for files without a mapped extension, by shape — an RFC 5322 header block or an mbox From separator — so a .txt that happens to start with From: stays text (D-012).
| Format | Detect | Text (extract_text) |
convert to |
inspect detail |
mail |
|---|---|---|---|---|---|
| eml | .eml; header block (unmapped ext) |
key headers, blank line, plain body (html flattened when no plain part), attachment: lines |
md html txt pdf (weasyprint) | from/to/cc, date (ISO), subject, message id, in-reply-to, references, parts, has_html, attachments | attachments, threads |
| mbox | .mbox .mbx; From line + header block (unmapped ext) |
# <subject> block per message |
md txt | messages, first/last date, top senders | attachments, split, threads |
| pst/ost | — (input to mail pst only) |
— | eml or mbox per folder via readpst |
— | pst |
Outlook .msg item files are cut (D-011): no pure-Python writer exists, so the support-matrix test could not hold a generated fixture; export to .pst or save as .eml instead.
Explicit scope notes¶
- Bulk moves refuse to rename files git is tracking.
rename --apply,organize --apply,intake --applyandwatch --done-dir/--error-direxit 2 when the move would touch a tracked path, naming the repository and what it found; each takes--allow-tracked(--forceis a deprecated alias that warns, D-022). Untracked files inside a repository are fine, so~/Downloadsunder a dotfiles repo keeps working — the guard asks "is it tracked?", not "is it in a repo?", because the second question refuses cases with no way out and teaches the override reflex it exists to prevent. The dry-run default is never guarded, nor is a destination that does not exist yet. Without the git binary the question cannot be answered, so carrel exits 3 with git's install hint rather than guessing. This exists because arename --applyaimed at carrel's own checkout renamed 21 tracked files (spec 29, D-017). Git tracking is the only hazard carrel detects: unambiguous to check and catastrophic to break. - PDF redaction is true redaction (rasterization destroys the text layer) — documented tradeoff; searchability restorable via
carrel ocrafterwards. - html thumbnails go through weasyprint→pdf→pdftoppm; if weasyprint missing, degrade with hint.
- Near-dupe uses a dependency-free dHash (no numpy/imagehash) to keep install light.
- Token counts in
packdefault to a chars/3.6 heuristic labeledtokens_est;--tokenizer exactcounts with tiktokeno200k_base(labeledtokens, the header names the tokenizer) and needs thetokensextra — no network, and the heavy dependency stays opt-in. pack --queryranks indexed files, and since spec 22carrel indexcovers plain-text source and config files (.py,.rs,.toml,.yaml,Makefile, …) as typecode, so query-driven packing works on source trees as well as document trees.--no-sourceindexes documents only. What is still never walked: hidden entries (.git,.carrel, dotfiles),.gitignored paths (--no-gitignoreopts out), and binary types with no extractable text.- Relevance scores are bm25 from FTS5: lower is better, and on small documents they are tiny (≈ -1.5e-06 in the proofs), so the human
scorecolumn can print-0.000while--jsoncarries the full value. - Spreadsheets are read-only (xlsx → csv/json); writing xlsx and reading legacy
.xls/.docare out of scope.
Cuts (running log — updated through the build)¶
recipesrunner: stretch, cut if time is short (cookbook shell scripts cover the use cases).- PAdES cryptographic PDF signing: cut to stretch; visible stamp + gpg manifest signing ship instead (rationale: key management UX exceeds session scope).
- v0.4.0: Outlook
.msg(would need theextract-msgpackage and a hand-made OLE fixture) — cut;mail pstvia readpst covers Outlook exports. Also deferred from the v0.4.0 brainstorm: ledger reconciliation (recon), folder roll-ups (ledger), embedded metadata write-back (meta embed), watch rules files, snapshot diffs (changes), mail drafting and sending. - v0.2.0: unwired adapter entries removed (
gs,pngquant,jq,mlr,rg,fd,sqlite3,inotifywait,claude) — none was referenced by any command, yetdoctoradvertised them. Re-add each together with the command that uses it (spec 19).