{"id":50467849,"url":"https://github.com/darkliquid/tilbo","last_synced_at":"2026-06-01T08:30:49.966Z","repository":{"id":346744854,"uuid":"1170288131","full_name":"darkliquid/tilbo","owner":"darkliquid","description":"Tag-first file management platform for Linux","archived":false,"fork":false,"pushed_at":"2026-03-25T09:48:16.000Z","size":2233,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-26T12:05:20.587Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/darkliquid.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-02T00:30:24.000Z","updated_at":"2026-03-25T09:47:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/darkliquid/tilbo","commit_stats":null,"previous_names":["darkliquid/tilbo"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/darkliquid/tilbo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkliquid%2Ftilbo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkliquid%2Ftilbo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkliquid%2Ftilbo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkliquid%2Ftilbo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/darkliquid","download_url":"https://codeload.github.com/darkliquid/tilbo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darkliquid%2Ftilbo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33767434,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-01T08:30:47.984Z","updated_at":"2026-06-01T08:30:49.958Z","avatar_url":"https://github.com/darkliquid.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tilbo Taggings\n\n_What has it gots in it's pocketses?_\n\nTilbo is a filesystem tagging and metadata system that provides a way to tag\nfiles manually and automatically, and extract and associate metadata with them.\n\nThis data can then be used for file navigation using FUSE or a simple IPC system.\n\nIn addition to the daemon that maintains the tags and metadata, there is a CLI\ntool for tagging and a Quickshell GUI file browser that communicates with the\ndaemon over a Unix socket (JSON RPC).\n\nTags and metadata are stored via extended filesystem attributes by default, with\na fallback to storing the data in an sqlite database for filesystems that do not\nsupport extended filesystem attributes.\n\n---\n\n## Contents\n\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Getting Started](#getting-started)\n- [The Daemon](#the-daemon)\n- [The CLI](#the-cli)\n- [The GUI Browser](#the-gui-browser)\n- [FUSE Virtual Filesystem](#fuse-virtual-filesystem)\n- [Auto-tagging: Harvesters and Rules](#auto-tagging-harvesters-and-rules)\n- [Optional External Dependencies](#optional-external-dependencies)\n- [Configuration](#configuration)\n- [Limitations](#limitations)\n\n---\n\n## Requirements\n\n- Linux (kernel 5.10+ for fanotify; 5.17+ recommended for rename tracking)\n- Go 1.26 or later (to build from source)\n- FUSE kernel module (`fuse` package)\n- [Quickshell](https://quickshell.outfoxxed.me/) (optional — only required for the GUI browser)\n\n---\n\n## Installation\n\n```sh\n# Build the unified tilbo binary (CGo-free; no special deps needed)\ngo build -o tilbo ./cmd/tilbo\n\n# Install to ~/bin (or /usr/local/bin)\ncp tilbo ~/bin/\n\n# The GUI browser is a pure-QML app — no Go build step needed.\n# Install Quickshell (https://quickshell.outfoxxed.me/), then run directly:\n#   quickshell -p internal/quickshell/shell.qml\n\n# Generate shell completions\ntilbo completion bash \u003e ~/.local/share/bash-completion/completions/tilbo\n\n# Write baseline config files from current flags\ntilbo --socket /run/user/$UID/tilbo.sock config init\ntilbo daemon --watch \"$HOME\" --log-level info config init\n```\n\nA systemd user service unit is recommended for the daemon:\n\n```ini\n# ~/.config/systemd/user/tilbo-daemon.service\n[Unit]\nDescription=Tilbo tag-first file manager daemon\nAfter=network.target\n\n[Service]\nExecStart=%h/bin/tilbo daemon --watch %h --log-format text --log-level info\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n```\n\n```sh\n# Install user-mode units via CLI using standard systemd tooling\ntilbo daemon systemd install\n\n# Or manually, if preferred\nsystemctl --user enable --now tilbo-daemon\n```\n\n---\n\n## Getting Started\n\n1. **Start the daemon** (watching your home directory):\n\n   ```sh\n   tilbo daemon --watch ~ --fuse-mount ~/tags\n   ```\n\n2. **Open the GUI browser** (optional, requires Quickshell):\n\n   ```sh\n   tilbo gui\n   ```\n\n3. **Tag a file**:\n\n   ```sh\n   tilbo tag add ~/documents/report.pdf work project-alpha\n   ```\n\n4. **Browse by tag** (after FUSE is mounted):\n\n   ```sh\n   ls ~/tags/work/\n   ls ~/tags/work+project-alpha/\n   ```\n\n5. **Search from the CLI**:\n\n   ```sh\n   tilbo search --tags work+project-alpha\n   tilbo search --tags @recent\n   ```\n\nTags are stored directly in the file's extended attributes (`user.tags`). The\nSQLite index is a read cache rebuilt from xattrs on startup — you can delete\nand recreate it without losing any tag data.\n\n---\n\n## The Daemon\n\n`tilbo daemon` is the core engine. It watches a directory tree via fanotify,\nruns a metadata harvester pipeline, stores results in a SQLite index, serves a\nFUSE virtual filesystem, and exposes a Unix socket IPC endpoint.\n\n### Flags\n\n| Flag | Default | Description |\n| --- | --- | --- |\n| `--watch \u003cpath\u003e` | `~` | Filesystem path to watch via fanotify |\n| `--db \u003cpath\u003e` | `~/.local/state/tilbo/index.db` | SQLite index database path |\n| `--fuse-mount \u003cpath\u003e` | `/run/user/$UID/tilbo/tags` | FUSE virtual filesystem mount point (empty to disable) |\n| `--log-format \u003cfmt\u003e` | `text` | Log format: `text` or `json` |\n| `--log-level \u003clvl\u003e` | `info` | Log level: `debug`, `info`, `warn`, `error` |\n| `--embed-disabled` | `false` | Disable vector embeddings entirely |\n| `--embed-model \u003cpath\u003e` | | Path to local ONNX model directory (overrides auto-download) |\n| `--embed-model-name \u003cname\u003e` | `sentence-transformers/all-MiniLM-L6-v2` | Huggingface model to auto-download |\n| `--watch-hidden` | `false` | Watch hidden files and directories |\n| `--watcher \u003cbackend\u003e` | `auto` | Filesystem watcher backend: `auto`, `fanotify`, `inotify` |\n\n### Config and Shell Setup\n\n```sh\n# Write a baseline daemon config using current flags\ntilbo daemon --watch \"$HOME\" --db \"$HOME/.local/state/tilbo/index.db\" config init\n\n# Install user-mode systemd service and socket\ntilbo daemon systemd install\n\n# Generate shell completions\ntilbo completion zsh \u003e ~/.zfunc/_tilbo\n```\n\nThe generated `~/.config/tilbo/config.toml` also includes a `[browser]` section.\nCommon browser settings can be edited there:\n\n```toml\n[browser]\nuse_trash = true\ninline_thumbnails = true\nauto_properties_slideout = false\ntheme = \"nord\" # supported presets: nord, light\n\n[browser.keybindings]\nback = \"Alt+Left\"\nforward = \"Alt+Right\"\nup = \"Alt+Up\"\nhome = \"Alt+Home\"\ntoggle_hidden = \"Ctrl+H\"\ntoggle_grid = \"Ctrl+G\"\nrefresh = \"F5\"\nfocus_path = \"Ctrl+L\"\ndelete = \"Delete\"\npermanent_delete = \"Shift+Delete\"\ncopy = \"Ctrl+C\"\ncut = \"Ctrl+X\"\npaste = \"Ctrl+V\"\nnew_folder = \"Ctrl+Shift+N\"\nselect_all = \"Ctrl+A\"\nzoom_in = \"Ctrl++\"\nzoom_in_alternate = \"Ctrl+=\"\nzoom_out = \"Ctrl+-\"\nzoom_reset = \"Ctrl+0\"\n```\n\n### Watcher Permissions and Fallback Modes\n\n`tilbo daemon` prefers fanotify, but fanotify setup depends on kernel and runtime permissions.\n\n- `CAP_SYS_ADMIN` is required for fanotify mark setup and FID handle resolution.\n- On a normal supported mount, the daemon uses full fanotify mode.\n- If `FAN_MARK_FILESYSTEM` fails with `EXDEV` (common with btrfs subvolumes), the daemon switches to a hybrid mode:\n  - fanotify mount marks for write/modify notifications,\n  - inotify for create/delete/move events under the configured watch root.\n- If fanotify is unavailable (for example missing capability), the daemon falls back fully to inotify.\n\nTo grant capability to a local build:\n\n```sh\nsudo setcap cap_sys_admin+ep ./tilbo\n```\n\nVerify capability:\n\n```sh\ngetcap ./tilbo\n```\n\nNote: rebuilding the binary clears file capabilities, so re-apply `setcap` after each rebuild.\n\n### Signals\n\n| Signal | Behaviour |\n| --- | --- |\n| `SIGTERM` / `SIGINT` | Graceful shutdown; unmounts FUSE; closes index |\n| `SIGHUP` | Reloads harvester and rule configuration; triggers background re-evaluation sweep |\n\n### Data Storage\n\n#### xattrs (source of truth)\n\nTags are stored in `user.tags` as a space-separated list. Metadata key/value\npairs are stored in `user.meta.\u003ckey\u003e`. Tag provenance (which rule applied which\ntag) is stored in `user.tags.source` as JSON.\n\n#### SQLite index (cache)\n\nA SQLite database caches the xattr data for fast search and graph queries. It\nincludes an FTS5 virtual table for full-text search over metadata values. The\nindex is fully rebuildable from xattrs — the daemon performs a background scan\non startup to ensure consistency.\n\n#### Sidecar fallback\n\nFor filesystems that do not support xattrs (FAT32, some NFS mounts), the daemon\nfalls back to a sidecar SQLite database at `~/.local/share/tilbo/sidecar.db`\nkeyed by inode and device number.\n\n---\n\n## The CLI\n\n`tilbo` is the terminal client. All commands communicate with a running\ndaemon via the Unix socket at `/run/user/$UID/tilbo.sock`.\n\n### Tag management\n\n```sh\n# Add tags to a file\ntilbo tag add \u003cpath\u003e \u003ctag\u003e [tag...]\n\n# Remove tags from a file\ntilbo tag remove \u003cpath\u003e \u003ctag\u003e [tag...]\n\n# List a file's current tags\ntilbo tag list \u003cpath\u003e\n```\n\n### Search\n\n```sh\n# Find files matching a tag expression\ntilbo search --tags \u003cexpr\u003e\n\n# Find files matching ANY of the given tags\ntilbo search --tags tag1,tag2 --any\n\n# Exclude files with specific tags\ntilbo search --tags work --exclude archived\n\n# Full-text search over metadata values\ntilbo search --fts \"invoice 2024\"\n\n# Filter by a metadata key/value pair\ntilbo search --meta \"codec=h265\"\n\n# Combine filters\ntilbo search --tags work --fts \"quarterly report\"\n\n# Control output, sorting, and pagination\ntilbo search --tags video --sort mtime:desc --limit 50 --offset 0 --format json\ntilbo search --tags photo --format tsv | cut -f1 | xargs ...\n```\n\n**Output formats:** `human` (default), `json`, `tsv`\n\n**Search options:**\n\n| Flag | Default | Description |\n| --- | --- | --- |\n| `--any` | `false` | Use OR semantics for `--tags` |\n| `--exclude \u003ctags\u003e` | | Comma-separated tags that must not be present |\n| `--fts \u003cquery\u003e` | | Full-text search over metadata values |\n| `--meta \u003cfilter\u003e` | | Metadata filters: `key=op:value` (e.g. `iso=gt:1600`) |\n| `--sort \u003corder\u003e` | `mtime:desc` | Sort order: `field:asc|desc` (`mtime`, `name`, `size`) |\n| `--limit \u003cn\u003e` | `50` | Maximum results to return |\n| `--offset \u003cn\u003e` | `0` | Result offset for pagination |\n| `--format \u003cfmt\u003e` | `human` | Output format: `human`, `json`, `tsv` |\n\n**Tag expression syntax:**\n\n| Expression | Meaning |\n| --- | --- |\n| `work` | Files tagged `work` |\n| `work+project` | Files tagged both `work` AND `project` |\n| `work+project+!draft` | Files tagged `work` AND `project` but NOT `draft` |\n| `low-priority` | Files tagged `low-priority` (hyphens are literal in tag names) |\n| `work,personal` | Files tagged `work` OR `personal` |\n| `@recent` | Files modified in the last 7 days |\n| `@recent:30d` | Files modified in the last 30 days |\n| `@untagged` | Files with no tags |\n| `@search:invoice 2024` | Full-text search over metadata values |\n| `@similar:/path/to/file` | Files similar via tag graph and vector embeddings |\n| `@meta:iso:gte:1600` | Files where the `iso` metadata key is ≥ 1600 |\n\nNotes:\n\n- `+` (AND) and `,` (OR) cannot be mixed in the same expression.\n- `-` is a literal character in tag names: `low-priority` means the tag named `low-priority`.\n- NOT uses the `!` prefix on an individual term: `work+!draft`.\n- Tag names containing `+`, `,`, `!`, or `%` must be percent-encoded (`%2B`, `%2C`, `%21`, `%25`) in FUSE paths. The CLI and IPC do not require encoding.\n\n### Metadata\n\n```sh\n# Show all metadata for a file\ntilbo meta show \u003cpath\u003e\ntilbo meta show \u003cpath\u003e --format json\n\n# Set a metadata key\ntilbo meta set \u003cpath\u003e \u003ckey\u003e \u003cvalue\u003e\n\n# Delete a metadata key\ntilbo meta delete \u003cpath\u003e \u003ckey\u003e\n```\n\n### Related files\n\n```sh\n# Find files related to a given file via tag graph and vector embeddings\ntilbo related \u003cpath\u003e\ntilbo related \u003cpath\u003e --limit 20 --hops 3 --vec-weight 0.8\ntilbo related \u003cpath\u003e --format json\n```\n\nRelated files are ranked by a combination of:\n1.  **Tag Graph Traversal:** A weighted IDF score across shared tags, decayed by hop distance. Tags shared by many files contribute less to the score. High-cardinality tags (shared by more than 5% of all indexed files) are skipped.\n2.  **Vector Similarity:** If vector embeddings are enabled (default), a cosine similarity boost is applied to files found during traversal.\n\n| Flag | Default | Description |\n| --- | --- | --- |\n| `--hops \u003cn\u003e` | `3` | Maximum graph hops from seed |\n| `--hop-weight \u003cw\u003e` | `1.0` | Weight multiplier for graph hop distance |\n| `--vec-weight \u003cw\u003e` | `0.4` | Weight multiplier for vector similarity |\n| `--limit \u003cn\u003e` | `20` | Maximum results to return |\n| `--format \u003cfmt\u003e` | `human` | Output format: `human`, `json`, `tsv` |\n\n### Daemon management\n\n```sh\n# Check daemon status\ntilbo daemon status\n\n# Reload harvester and rule configuration (equivalent to SIGHUP)\ntilbo daemon reload-rules\n```\n\n### Config and shell completions\n\n```sh\n# Write a baseline CLI config using current flags\ntilbo --socket /run/user/$UID/tilbo.sock config init\n\n# Generate shell completions\ntilbo completion fish \u003e ~/.config/fish/completions/tilbo.fish\n```\n\n---\n\n## The GUI Browser\n\nThe Quickshell frontend (`internal/quickshell`) is a pure-QML file browser\nthat communicates with a running daemon over a newline-delimited JSON Unix\nsocket (`$XDG_RUNTIME_DIR/tilbo-ui.sock`). It requires\n[Quickshell](https://quickshell.outfoxxed.me/) to be installed.\n\n### Running\n\n```sh\n# Start the daemon first\ntilbo daemon --watch ~ --fuse-mount ~/tags\n\n# Then launch the browser in another terminal (or via autostart)\ntilbo gui\n\n# Or with the mise task shorthand\nmise run run-quickshell\n```\n\nBy default the properties sidebar stays closed until you click the `PROPERTIES`\nstrip. Set `browser.auto_properties_slideout = true` in `config.toml` if you\nwant it to open automatically on selection.\n\n### Layout\n\n| Area | Description |\n| --- | --- |\n| Header | Search bar (chips for tags, globs, full-text), grid/list toggle, hidden-files toggle |\n| Left sidebar (Places) | Home directory, XDG user dirs, FUSE tag mount when active |\n| Main pane | File grid or list; double-click to navigate/open |\n| Right sidebar (Properties) | Name, path, size, mtime, metadata key/value pairs, tag badges for the selected file |\n| Footer | Clickable breadcrumb strip; click the last segment or the ✎ icon to type a path directly |\n\n### Search chip syntax\n\n| Chip | Behaviour |\n| --- | --- |\n| `photo` | Indexed tag search — files tagged `photo` |\n| `glob:*.jpg` | Filesystem glob search |\n| `fts:sunset` | Full-text search over metadata values |\n| `hidden:any` | Include hidden files in results |\n\nMultiple chips are combined: tag chips use AND semantics; glob chips run a\nseparate filesystem walk; results from both are merged.\n\n### Live events\n\nThe browser reacts to live daemon events pushed over the UI socket without\npolling:\n\n| Event | Browser reaction |\n| --- | --- |\n| `FileTagged` | Tag badges on the affected entry update in-place |\n| `IndexUpdated` | Active search re-executes with the latest index |\n| `DaemonStateChanged` | Connection indicator in the search bar updates |\n\n---\n\n## FUSE Virtual Filesystem\n\nWhen `--fuse-mount` is set (default `~/tags`), the daemon mounts a virtual\nfilesystem that presents your files organised by tags rather than filesystem\nlocation.\n\n### Path grammar\n\n```text\n~/tags/\u003cexpr\u003e/                    — virtual directory for a tag expression\n~/tags/work/                      — all files tagged \"work\"\n~/tags/work+project/              — files tagged both \"work\" AND \"project\"\n~/tags/work+project+!draft/       — \"work\" AND \"project\" AND NOT \"draft\"\n~/tags/low-priority/              — files tagged \"low-priority\" (hyphens allowed)\n~/tags/work,personal/             — files tagged \"work\" OR \"personal\"\n~/tags/@recent/                   — files modified in the last 7 days\n~/tags/@recent:30d/               — files modified in the last 30 days\n~/tags/@untagged/                 — files with no tags\n~/tags/@search:foo bar/           — full-text metadata search\n~/tags/@similar:/real/path/       — graph-similar files\n~/tags/@meta:iso:gte:1600/        — metadata filter\n~/tags/@browse/                   — incremental tag browser (see below)\n~/tags/@browse/work/              — lists tags co-occurring with \"work\"\n~/tags/@browse/work/project/      — lists tags co-occurring with work AND project\n~/tags/@browse/work/!draft/       — excludes \"draft\" from accumulated query\n~/tags/@browse/work/@files/       — files matching the current accumulated query\n```\n\n### How it works\n\nEach entry in a virtual directory is a **symlink** to the real file on disk.\nReads and writes go to the actual file. Setting xattrs on a virtual-directory\nentry applies them to the real file and updates the index.\n\n**Rename semantics:** Moving a file from one virtual directory to another applies\ntag changes rather than a filesystem rename:\n\n```sh\n# Adds tag \"personal\", removes tag \"work\"\nmv ~/tags/work/report.pdf ~/tags/personal/\n```\n\nRename only works when both source and destination are simple `+`/`-` tag\nexpressions. Rename within the same directory is a no-op. Moving files out of\nthe mount entirely returns `EXDEV`.\n\n**Inode stability:** Inodes are derived from a 64-bit FNV hash of the real\nabsolute path, ensuring that directory listings remain stable across daemon\nrestarts.\n\n**Deduplication:** When multiple files have the same basename, the virtual\ndirectory appends a `_2`, `_3`, … suffix to avoid collisions.\n\n### Incremental tag browser (@browse)\n\n`@browse` is designed for interactive exploration with a file manager or shell.\nRather than requiring you to know the full tag query upfront, each subdirectory\nlevel shows only the tags that co-occur with all the tags you have navigated so\nfar — narrowing the visible set with every step.\n\n```text\n~/tags/@browse/              — lists all tags\n~/tags/@browse/work/         — lists tags that appear alongside \"work\"\n~/tags/@browse/work/video/   — lists tags that appear with both \"work\" AND \"video\"\n~/tags/@browse/work/@files/  — the matching files (symlinks)\n```\n\nPrefix a tag name with `!` to exclude it:\n\n```text\n~/tags/@browse/video/!draft/@files/   — video files, excluding drafts\n```\n\n`@files` is always present at every level and shows the files matching the\naccumulated query so far. Tag names with special characters are percent-encoded\nin directory listings (same rules as the flat grammar).\n\n### Integration tips\n\n```sh\n# Use with fzf for interactive tag-browsing file picker\nls ~/tags/work/ | fzf\n\n# Open a tag-filtered view in your file manager\nxdg-open ~/tags/work+project/\n\n# Add tag virtual dirs as GTK bookmarks\necho \"file://$HOME/tags/work work\" \u003e\u003e ~/.config/gtk-3.0/bookmarks\n```\n\n---\n\n## Auto-tagging: Harvesters and Rules\n\nThe daemon automatically extracts metadata from files and applies tags based on\nconfigurable rules.\n\n### Pipeline overview\n\n1. A filesystem event triggers the pipeline for a file.\n2. All matching **harvesters** run concurrently and produce a metadata map\n   (MIME type, dimensions, duration, EXIF data, etc.).\n3. The **rule engine** evaluates the metadata map and writes tags to the file's\n   xattrs and the SQLite index.\n4. If you manually remove a rule-applied tag, that override is recorded. The rule\n   will not reapply the tag until you clear the override.\n5. Sending `SIGHUP` (or running `tilbo daemon reload-rules`) reloads all rule\n   files and triggers a background re-evaluation sweep over all indexed files.\n\n### Writing harvester plugins\n\nHarvesters are registered via drop-in TOML files in `~/.config/tilbo/harvesters/`.\n\n**`~/.config/tilbo/harvesters/my-harvester.toml`:**\n\n```toml\n[harvester]\nname        = \"my-harvester\"\ncommand     = [\"/usr/local/bin/my-harvester\"]\n# or WASM:  = [\"~/.local/share/tilbo/harvesters/my-harvester.wasm\"]\nmime_filter = [\"video/*\"]        # only run on matching MIME types\npath_glob   = []                 # alternative: file glob patterns\npriority    = 50                 # lower runs first; built-ins are 0\ntimeout_ms  = 5000\nasync       = true               # don't block rule evaluation\n```\n\nThe harvester receives JSON on stdin and must write JSON to stdout:\n\nstdin:\n\n```json\n{\n  \"path\":     \"/home/user/video.mkv\",\n  \"mime\":     \"video/x-matroska\",\n  \"existing\": { \"user.tags\": \"work\" }\n}\n```\n\nstdout:\n\n```json\n{\n  \"width\": 1920,\n  \"height\": 1080,\n  \"duration_seconds\": 5400,\n  \"codec\": \"h265\",\n  \"hdr\": true\n}\n```\n\nExit 0 → output merged into metadata map. Exit non-zero → output ignored.\nKeys beginning with `_` are internal and not written to xattr.\n\n### Writing declarative rules (TOML)\n\nRules live in `~/.config/tilbo/rules/\u003cname\u003e.toml` (or `/etc/tilbo/rules/`).\n\n```toml\n[[rule]]\nname = \"hd-video\"\ntags = [\"video\", \"HD\"]\n\n[rule.match]\nmime = \"video/*\"\n\n[rule.match.width]\ngte = 1280\n\n\n[[rule]]\nname = \"large-file\"\ntags = [\"large\"]\n\n[rule.match.size_bytes]\ngte = 1073741824        # 1 GiB\n\n\n[[rule]]\nname = \"old-document\"\ntags = [\"archive\"]\n\n[rule.match]\nmime = \"application/pdf\"\n\n[rule.match.mtime]\nbefore = \"2015-01-01\"\n```\n\n**Condition operators:** `eq`, `glob`, `gte`, `lte`, `gt`, `lt`, `between`,\n`in`, `not`, `before`, `after`. Add `any = true` at the rule level for OR\nsemantics (default is AND across all conditions).\n\n### Writing scripted rules (Lua)\n\n```lua\n-- ~/.config/tilbo/rules/video-quality.lua\nfunction apply(meta)\n  if not meta.mime or not meta.mime:match(\"^video/\") then\n    return {}\n  end\n\n  local tags = {\"video\"}\n\n  if meta.width then\n    if meta.width \u003e= 3840 then\n      tags[#tags+1] = \"4K\"\n      tags[#tags+1] = \"HD\"\n    elseif meta.width \u003e= 1280 then\n      tags[#tags+1] = \"HD\"\n    end\n  end\n\n  return tags\nend\n```\n\nThe `apply(meta)` function receives the metadata map and returns a list of tags.\nThe sandbox has no filesystem or network access — only standard math, string, and\ntable libraries are available.\n\n### Testing and validation\n\n```sh\n# List all active harvesters\ntilbo harvester list\n\n# Test the harvester pipeline against a specific file\ntilbo harvester test ~/photos/vacation.jpg\n\n# List all configured rules\ntilbo rule list\n\n# Validate rule syntax and configuration\ntilbo rule validate\n\n# Test rule evaluation against a file (shows what tags would be applied)\ntilbo rule test ~/photos/vacation.jpg\n```\n\n---\n\n## Optional External Dependencies\n\nThe daemon's built-in harvester pipeline works without any external tools.\nThe following optional binaries can be installed to enable additional metadata\nextraction. The daemon detects them at startup and logs which are active.\n\n| Binary | Purpose | Install |\n| --- | --- | --- |\n| `ffprobe` | Richer video/audio metadata (codec, bitrate, frame rate, HDR, stream details) — overrides the built-in media harvester | Part of [FFmpeg](https://ffmpeg.org/download.html); most distros: `ffmpeg` package |\n| `ebook-meta` | Ebook metadata for MOBI, AZW, AZW3, FB2, and other formats Calibre supports; also enriches EPUB with series/rating data | Part of [Calibre](https://calibre-ebook.com/download); most distros: `calibre` package |\n| `magika` | ML-based file-type detection — improves MIME accuracy for ambiguous files (Office formats, polyglot files, obscure text variants) | `pip install magika` or [pre-built release](https://github.com/google/magika/releases) |\n\n### Why these are optional\n\nAll core metadata (EXIF/IPTC from images, PDF info, MP4/MKV/audio duration and\ntags, EPUB title/author/ISBN) is extracted in-process using pure-Go libraries —\nno external tools required. The optional binaries exist only to provide deeper\nor higher-accuracy results for specific file categories when they are already\npresent on the system.\n\n---\n\n## Configuration\n\n### File locations\n\n| Path | Purpose |\n| --- | --- |\n| `~/.local/state/tilbo/index.db` | SQLite index (default; override with `--db`) |\n| `~/.local/share/tilbo/sidecar.db` | Sidecar store for non-xattr filesystems |\n| `~/.config/tilbo/harvesters/*.toml` | User harvester registrations |\n| `/etc/tilbo/harvesters/*.toml` | System-wide harvester registrations |\n| `~/.config/tilbo/rules/*.toml` | User TOML rules |\n| `~/.config/tilbo/rules/*.lua` | User Lua rules |\n| `/etc/tilbo/rules/*.toml` | System-wide TOML rules |\n| `~/.local/lib/tilbo/plugins/*.so` | Native plugin harvesters |\n| `/usr/lib/tilbo/plugins/*.so` | System-wide native plugins |\n| `/run/user/$UID/tilbo.sock` | IPC Unix socket |\n| `/run/user/$UID/tilbo/tags` | FUSE mount point (default; override with `--fuse-mount`) |\n\n### Wasm plugin cache\n\nWASM modules are compiled once and cached in the OS temp directory\n(`$TMPDIR/tilbo-wasm-cache`). This avoids per-invocation compilation overhead.\nDelete the cache directory to force recompilation.\n\n---\n\n## Limitations\n\n### Kernel requirements\n\n- **fanotify** requires Linux kernel 5.10 or later.\n- **Rename event tracking** (`FAN_RENAME`) requires kernel 5.17 or later. On\n  older kernels, the daemon falls back to tracking moves via `FAN_MOVED_FROM` /\n  `FAN_MOVED_TO` pairs, which can miss cross-directory renames under high\n  concurrency.\n\n### xattr support\n\n- Extended attributes are not supported on FAT32, exFAT, or some network\n  filesystems (NFSv3 without server config, SMB by default). The daemon detects\n  this at startup and falls back to a sidecar SQLite database. The sidecar is\n  keyed by inode+device, so it is invalidated if files are moved between\n  filesystems.\n- xattrs on Linux are typically capped at 64 KiB per namespace per file. Files\n  with very large numbers of tags or long metadata values may hit this limit.\n\n### FUSE\n\n- The FUSE mount is read-only in the sense that creating new files inside a\n  virtual directory is not supported — files must exist in the real filesystem\n  first. Writing to existing files (reads/writes) passes through to the real file.\n- **Rename** only works when both the source and destination directories are\n  simple tag expressions (no OR expressions, no special `@` directives). Renaming\n  within complex expressions returns `EPERM`.\n- Directory listings cache for 2 seconds (entry TTL). Index changes from other\n  processes may take up to 2 seconds to appear.\n- The FUSE mount requires the `fuse` kernel module. It is incompatible with\n  user namespaces that don't have `CAP_SYS_ADMIN`.\n\n### Graph traversal\n\n- Tags shared by more than 5% of all indexed files are treated as stopwords and\n  skipped during graph traversal. This prevents very common tags (`document`,\n  `work`) from dominating related-file results, but means those tags do not\n  contribute to similarity scoring.\n- The BFS frontier is capped at `limit × 8` candidates per hop to bound\n  traversal cost. On very large corpora with dense tag graphs, some related files\n  reachable within the hop limit may not appear in results.\n- The graph is an in-memory snapshot loaded on daemon start and updated\n  incrementally. It does not persist across daemon restarts (always rebuilt from\n  the index).\n\n### Auto-tagging\n\n- Rule overrides (when you manually remove a rule-applied tag) are stored\n  per-file in the index. If you delete and recreate the index, overrides are\n  lost and rules will reapply previously suppressed tags.\n- Harvester processes run with a configurable timeout (`timeout_ms`). Files\n  that harvesters cannot process within the timeout are indexed with whatever\n  metadata was available at the time.\n- WASM and subprocess harvesters have WASI stdio only — no filesystem or\n  network access from within the sandbox.\n\n### Vector embeddings\n\n- The embedding pipeline using `knights-analytics/hugot` and ONNX runs locally. The first run will automatically download the default model (`all-MiniLM-L6-v2`) unless configured otherwise.\n- Semantic similarity search via `@similar:` or `tilbo related` combines tag graph traversal with vector similarity.\n- Vector search requires the `sqlite-vec` extension to be available to the daemon's SQLite driver (built-in by default).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkliquid%2Ftilbo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdarkliquid%2Ftilbo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarkliquid%2Ftilbo/lists"}