diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..8adf0df --- /dev/null +++ b/SKILL.md @@ -0,0 +1,100 @@ +--- +name: otag +description: >- + Teaches how to tag, search and organize files and folders with otag (OrvaniTagger, https://github.com/randogoth/orvanitag), a todo.txt style tagger that stores +projects, @contexts, #tags and a note per file in plain `.otag` files next to them. Use this skill whenever a user asks to tag, label, categorize, find or list files by tag, bulk-retag, move or copy files together with their tags, sync tags into files (ID3, PDF, Office, ebooks, archives, images, source comments, front matter) or into TagSpaces, or when a folder contains `.otag` or `.otag.toml` files. Install by copying this file to a skill directory, e.g. ~/.claude/skills/otag/SKILL.md. +--- + +# Tagging files with otag + +otag gives every file and folder todo.txt style categories: `+project`, `@context`, `#tag`, plus a free text note. There is no database. Each folder keeps the entries of its files and subfolders in a plain text `.otag` file, so tags travel with the folder when it is copied, synced or archived. The command is `otag`, installed e.g. with `uv tool install 'orvanitag[all]'`. + +## Rules that matter for agents + +1. **Tagging means the `.otag` sidecar files, never the files themselves.** Use `add`, `remove`, `cp`, `mv` and `rm` for every tagging request. Run `write` (including `write tagspaces`) only when the user explicitly asks to embed tags in files or sync them to TagSpaces, and only for the files they named; never as a side effect, a "helpful" extra step, or because a plugin exists for the file type. +2. **Your shell is not a terminal, so `otag ls` prints bare paths only**, one per line, with no tags. That is meant for piping. To see the tags themselves, read the `.otag` file in the file's folder (see *The .otag format*), e.g. `grep -F 'song.mp3 --- ' music/.otag`, or `find . -name .otag` to see them all. +3. **Quote every `#tag`** (`'#lofi'`), because an unquoted `#` starts a shell comment and the tag silently disappears. +4. **Words without a prefix become the note.** `otag add f.mp3 '#lofi' first take` sets the note to `first take`. Adding again merges the tags and *replaces* the note. `remove` can only remove projects, contexts and tags: to change a note, `add` a new one; to drop it, edit the `.otag` line. +5. **Searches match partially by default**: `#lo` finds `#lofi` and `#lowkey`. Use `-e` for whole terms before any bulk change based on a search. +6. **Ask the user first** before `purge` (deletes every `.otag` in a tree, irreversibly), `rm -f` / `mv -f` / `cp -f` (act on the files themselves) and `scan --replace` (overwrites entries). `write` needs an explicit request anyway (rule 1), because it modifies the files' contents. +7. Commands only accept **existing** files and folders. Flags go after the command. Errors print one line and exit with 1. + +## Commands + +Every command has a short alias. `otag -h` and `otag -h` show all arguments. + +| Command | Does | +|---|---| +| `add` (`a`) `FILE WORD...` | Adds projects, contexts, tags and a note to a file or folder | +| `remove` (`r`) `FILE [WORD...]` | Removes the given projects, contexts and tags; without any, the whole entry | +| `list` (`ls`, `l`) `[TERM...]` | Lists the tagged files under the current folder that match all terms; no terms list everything | +| `copy` (`cp`) / `move` (`mv`) `SRC DEST` | Copies or moves the tags to another path; `-f` also copies or moves the file | +| `del` (`rm`) `FILE` | Removes the entry; `-f` also deletes the file | +| `clean` (`c`) `[PATH]` | Removes entries of files that no longer exist | +| `purge` (`p`) `[PATH]` | Deletes all `.otag` and cache files in a tree (destructive) | +| `scan` (`s`) `[PATH]` | Adds tags embedded in files (and TagSpaces sidecars) to their entries; `--replace` makes them replace the entries | +| `write` (`w`) `PLUGIN FILE` | Writes a file's entry into the file itself; `-c` removes otag's embedded tags instead | +| `export` (`e`) `[FILE]` / `import` (`i`) `[FILE]` | Writes all entries of the file's folder tree to a `path --- tags` text file, and adds the missing tags back from one | + +`clean` and `scan` work recursively; `-n` stays in the given folder. `export` collects the tree the export file is in, so export to a file inside that tree. + +### Searching + +A term with a prefix only matches that category (`@drums`); a term without prefix matches any category and the note. All terms must match; `-or` matches any of them. Other flags: `-e` whole terms, `--files` or `-d` only files or folders, `-p PATH` searches another tree (paths are then printed with that prefix), `-n` skips subfolders, `-t` shows a table in a terminal. Results are relative to the current folder, shallow ones first, and folders have no trailing slash. + +```sh +otag ls '#lofi' @drums # files with both +otag ls -or +album +ep # files in either project +otag ls -e '#draft' --files # exactly #draft, files only +``` + +### Batches through pipes + +`add`, `remove`, `del` and `write` read paths from stdin, one per line, when the file is `-`. A failing path is reported without stopping the others, and the exit code is 1 at the end. A file literally named `-` is written `./-`. + +```sh +ls *.mp3 | otag add - +album '#lofi' +find . -name '*.pdf' | otag write pdf - +otag ls -e '#old' | otag add - '#new' # rename a tag: add the new one ... +otag ls -e '#old' | otag remove - '#old' # ... then remove the old one +``` + +`ls dir` prints names without `dir/`, so feed `ls dir/*` or `find dir` instead. + +## The .otag format + +One line per entry: the name relative to the folder, ` --- `, then projects, contexts, tags and the note, in that order. Folder names end with `/`. otag keeps the lines sorted by name and deletes the file once it is empty. + +``` +Boombap Loop.wav --- +album @drums #boombap #lofi +song.mp3 --- +album @drums #lofi #snare first take +stems/ --- @archive +``` + +Hand edits are fine as long as every line keeps ` --- `; a line without it makes otag stop with an error naming file and line, until it is fixed. Note words starting with `+`, `@` or `#` are read as tags. A tag is one word: spaces and commas are not possible. + +## Tags embedded in files + +Only use this section when the user explicitly asks for it (rule 1); normal tagging stays in the `.otag` files. `scan` reads tags stored inside files; `write PLUGIN FILE` stores a file's entry in it, replacing what that plugin wrote before, so removed tags disappear on the next `write`. `write -c` clears otag's embedded tags; it is never done implicitly. A file without entry can only be cleared, not written. + +| Plugin | Files | Stored in | +|---|---|---| +| `id3` | `.mp3` `.wav` `.flac` `.ogg` | The comment tag (needs the `audio` extra) | +| `pdf` | `.pdf` | The keywords field (`pdf` extra) | +| `office` | `.docx` `.xlsx` `.pptx` `.odt` `.ods` `.odp`, reading also `.doc` `.xls` `.ppt` | The description field | +| `ebook` | `.epub` `.fb2` `.cbz` | An otag line the readers ignore | +| `frontmatter` | `.md` `.markdown` `.txt` | `projects`, `contexts`, `tags`, `note` keys of the YAML front matter (`markdown` extra) | +| `comment` | Source code, `.html`, `.svg` | An `otag: ...` comment line near the top | +| `archive` | `.zip` `.tar` `.gz` `.tgz` | The archive or gzip comment, a pax header in tar | +| `image` | `.jpg` `.jpeg` `.png` `.gif` | An own JPEG, PNG or GIF comment | +| `tagspaces` | Every file and folder | The TagSpaces sidecar `.ts/.json` (`.ts/tsm.json` for folders); file name tags like `photo[a b].jpg` are read only | + +Plugins refuse rather than damage: a comment field already used by another tool is kept, and the error says so. An optional `.otag.toml` limits plugins for its folder tree (`plugins = ["id3", "pdf"]`, `[id3] types = [".mp3"]`, `[id3.map] tags = "genre"`). + +## Recipes + +- **Tag a file:** `otag add report.pdf +q3 @work '#finance' final version` +- **What is this file tagged with?** `grep -F 'report.pdf --- ' .otag` +- **Move a file and keep its tags:** `otag mv -f report.pdf archive/`. Moving or renaming with plain `mv` leaves the entry behind, and `otag mv` can't fix that afterwards, since it needs the source to exist. Instead, re-add the tags from the old `.otag` line at the new path, then run `otag clean` to drop the orphaned entry. +- **Import tags from file metadata or TagSpaces:** `otag scan music/` +- **Show otag's tags in TagSpaces** (only when asked): `otag ls | otag write tagspaces -` +- **Back up all tags of a tree:** `otag export tags.txt`, restore with `otag import tags.txt`