---
name: jotura-vault
description: Use this skill to read, search, or edit notes in the user's Jotura markdown vault. Triggers on explicit mentions of "Jotura", "my vault", "my notes", "my journal", "daily note"; questions whose answer lives in personal notes ("what did I write about X", "find that meeting note", "add Y to today's note"); requests to create, rename, delete, tag, or modify any note. Vault files are plain .md files on disk and MAY be edited directly, but the `jotura` CLI is the preferred, safe interface — it provides hash-CAS conditional writes, strict unique-match replaces, frontmatter-preserving edits, and search/trash/sync-consistent operations that raw sed/awk edits cannot. Do NOT use this skill for editing files outside the user's vault.
---

# Jotura vault

The user keeps their notes in a Jotura markdown vault — a plain directory of `.md` files (plus imported documents). You access it through the `jotura` CLI binary (`~/.cargo/bin/jotura`), which uses the same Rust core as the Jotura desktop app and guarantees atomic writes, round-trip Markdown fidelity, and hash-based conflict detection. Cloud sync (when the user has enabled it) is end-to-end encrypted, but on disk everything is plaintext — there is nothing to unlock.

The user often runs the Jotura desktop app concurrently. The CLI and desktop coordinate automatically — the desktop's file watcher picks up any change you make within ~1 second, and the CLI's `--if-hash` checks protect you from clobbering the desktop's writes.

You MAY read vault files directly (`cat`, `grep`, editors), but prefer the CLI for every mutation: it enforces unique-match replaces, preserves frontmatter, writes atomically, and honors `--if-hash` concurrency checks.

## Before you do anything

Run `jotura status --json` first (or `jotura doctor --json` for a deeper health check). It returns one of:

```json
{"vault": "/path/to/vault", "noteCount": 247, "sync": {"enabled": true, "keyCached": true, "paused": false}}
{"error": {"code": "NoVault", ...}}
```

- A `vault` path → you can proceed. Data commands never need a password.
- `NoVault` → ask the user to set `JOTURA_VAULT=/path/to/vault` in their shell, pass `--vault` to commands, or open a vault in the desktop app first.
- The `sync` block is informational: `enabled: false` means the vault is local-only; `keyCached: false` on a sync-enabled vault means uploads are paused until the user provides their sync password (`jotura sync login`, or unlock in the desktop). Reads and writes always work regardless.

## The canonical safe-edit workflow

Every edit should follow this pattern. The `--if-hash` check stops you from clobbering work the user did in the desktop app, in another CLI invocation, or that arrived via sync.

```sh
# 1. Read with a hash
HASH=$(jotura read "<path>" --json | jq -r .hash)

# 2. Edit using the hash as a precondition
jotura edit "<path>" \
  --replace "<exact old text>" \
  --with "<new text>" \
  --if-hash "$HASH"
```

If exit code is 2 (`HashConflict`), the file changed under you. The error JSON now includes a `currentVsIntended` unified-diff between what's on disk *right now* and what you were about to write, so you can decide whether to retry, merge, or abandon without re-reading:

```json
{
  "code": "HashConflict",
  "path": "notes/today.md",
  "expectedHash": "...",
  "actualHash": "...",
  "currentVsIntended": "@@ -1,3 +1,3 @@\n line 1\n-current line\n+intended line\n line 3\n"
}
```

### Previewing edits with `--dry-run`

Every mutating command (`write`, `edit`, `create`, `rename`, `delete`, `frontmatter set/delete`, `tag add/remove`, `batch`) accepts `--dry-run`. The command computes exactly what it would write, prints a structured preview, and does NOT touch disk or manifest. Use this when you're not sure an edit will hit the right text:

```sh
jotura edit notes/today.md \
  --replace "TODO" --with "DONE" --dry-run --json
# → { "dryRun": true, "currentHash": "...", "newHash": "...",
#     "changes": 1, "preview": { "kind": "replace", "before": "TODO", "after": "DONE" } }
```

Exit codes still fire as expected — a dry-run that *would have* failed with `HashConflict` (code 2) or `Ambiguous` (code 4) returns the same error without writing anything.

### Seeing the diff with `--diff`

Every mutating command also accepts `--diff`. The JSON output gets an extra `"diff"` field containing a standard unified-diff between the current and new content (3 lines of context, `---`/`+++`/`@@` headers). Combine with `--dry-run` to preview without writing:

```sh
jotura edit notes/today.md --replace "TODO" --with "DONE" --diff --dry-run --json
# → { ..., "diff": "@@ -3,3 +3,3 @@\n line a\n-TODO\n+DONE\n line c\n" }
```

For `create`, `rename`, and `delete` the diff is structural (a `(file renamed)` / `(file created)` / `(file deleted)` marker block) rather than a real text diff — there's no old-vs-new body to compare.

## Reading

```sh
# Body only (most common)
jotura read inbox/today.md

# With line numbers — needed when planning a line-based edit
jotura read inbox/today.md --numbered

# Structured: { path, hash, frontmatter, body, lineCount }
jotura read inbox/today.md --json

# Include the YAML frontmatter block
jotura read inbox/today.md --with-frontmatter
```

## Finding notes

```sh
# Full-text search across all notes (uses Tantivy; rebuilds index per call)
jotura search "deadline next week" --json

# Restrict results to a path prefix
jotura search "deadline" --in projects/ --json

# Filename-only fuzzy match — much faster than search
jotura quick-open "meeting" --json

# List all notes
jotura ls --json

# List notes in a folder
jotura ls --folder projects --json
```

### Exact-match search with `grep`

`jotura grep <pattern>` runs a regex over every note body. It's the
right tool when you need exact, predictable matches — `search` is fuzzy /
ranked and `quick-open` only looks at filenames.

```sh
# basic regex match — output is path:line:matched-text per match
jotura grep 'TODO[:\s]'

# case-insensitive
jotura grep --ignore-case 'unicorn'

# whole-line match
jotura grep --line-regexp '^# Daily'

# whole-word match
jotura grep --word-regexp 'jot'

# restrict to a folder via include glob
jotura grep 'TODO' --include 'inbox/*'

# only paths that contain a match
jotura grep 'TODO' --files-with-matches

# count matches per file
jotura grep 'TODO' --count

# stop after N matches per file
jotura grep 'TODO' --max-count 3

# JSON output: [{ path, line, lineText, matchedText }]
jotura grep 'TODO' --json
```

Flags: `-i`/`--ignore-case`, `-x`/`--line-regexp`, `-w`/`--word-regexp`,
`-c`/`--count`, `--max-count N`, `--include GLOB`, `--exclude GLOB`,
`-l`/`--files-with-matches`, `-L`/`--files-without-match`.

Templates (`.jotura/templates/`) and trashed notes are excluded
automatically. Cost: every note matching `--include` is read and
scanned — sub-second for a few thousand notes, several seconds for 10k+.
For fuzzy/ranked text search use `search`; for filename lookup use
`quick-open`.

## Finding links between notes

```sh
# List notes that link TO this path (backlinks)
jotura backlinks inbox/today.md --json

# List links inside a note (outgoing)
jotura links projects/x.md --json

# Cap results
jotura backlinks inbox/today.md --limit 20
```

Two link syntaxes are recognized:

- **Wikilinks:** `[[target]]` and `[[target|display text]]`
- **Markdown links:** `[display](target.md)` (the target must end in `.md`)

`#anchor` suffixes (`[[foo#heading]]`, `[foo](foo.md#h)`) are stripped
before matching.

Target resolution rules, tried in order:

1. **Exact path match.** `[[inbox/today.md]]` and `[[inbox/today]]` both
   match the note at `inbox/today.md`.
2. **Filename match.** `[[today]]` matches every note whose filename is
   `today.md`, regardless of folder. If two notes share a filename in
   different folders, *both* show up as backlinks for either one.

Title-based resolution (matching frontmatter `title:`) is **not**
implemented in v1.

Implementation: both commands scan every note per invocation —
brute force, no persistent index. Sub-second for typical vaults; expect
seconds for 10k+ notes.

## Working with documents

The vault holds two kinds of files: **notes** (`.md`, edited directly) and
**documents** (PDFs, Word docs, spreadsheets, images, anything else). Documents
are stored as plain files just like notes, but their bytes aren't searchable
on their own. So each document gets a **searchable markdown mirror** in the
md_store at `.md_store/<original-path>.<ext>.md` — produced by a bundled
converter. The mirror is a normal note carrying the converted text; the
original document stays untouched.

The md_store is excluded from `ls`, `quick-open`, and plain `search`, so it
never clutters note-level results.

```sh
# Import a document. Defaults to attachments/, then generates the md_store mirror.
jotura import ~/Downloads/contract.pdf --json
# → { "path": "attachments/contract.pdf", "contentHash": "...", "size": 12345,
#     "mdStorePath": ".md_store/attachments/contract.pdf.md", "converted": true }

# Choose a folder, or an explicit full vault path:
jotura import ~/Downloads/contract.pdf --folder projects/acme --json
jotura import ~/Downloads/contract.pdf --as projects/acme/signed-contract.pdf --json

# Import bytes from stdin (--as is required):
cat report.pdf | jotura import - --as attachments/report.pdf --json

# Skip mirror generation (store only the document itself):
jotura import data.bin --no-convert --json

# List documents with their conversion status:
jotura ls-documents --json
# → [{ "path": "attachments/contract.pdf", "size": 12345,
#      "hasMdStore": true, "conversionOk": true }]

# Regenerate mirrors (after editing a document, or to retry a failed conversion):
jotura regenerate-md-store attachments/contract.pdf --json
jotura regenerate-md-store --all --json
jotura regenerate-md-store --stale-only --json   # only the ones that need it
```

If the converter isn't installed yet, `import` still succeeds: it writes a stub
mirror with `converted: false` and a `note` field saying conversion was
deferred. The mirror self-heals once a converter is present (`regenerate-md-store
--stale-only`).

Need the raw bytes of a document? It's a plain file — read it straight from
the vault directory (`<vault>/attachments/contract.pdf`).

## Searching documents

When the user asks to search their documents (PDFs, Word docs, spreadsheets,
etc.) — as opposed to their notes — use `jotura search-documents <query>`. It
searches the markdown representations in the md_store and returns the ORIGINAL
document paths. Do NOT use plain `jotura search` for documents; that searches
notes and excludes the md_store. Do NOT try to read PDF/docx bytes directly;
read the converted markdown via `jotura read .md_store/<path>.<ext>.md` or rely
on search-documents snippets.

```sh
jotura search-documents "quarterly revenue" --json
# → [{ "documentPath": "attachments/q3-report.pdf",
#      "mdStorePath": ".md_store/attachments/q3-report.pdf.md",
#      "title": "q3-report", "snippet": "...quarterly <mark>revenue</mark>...",
#      "score": 0.92 }]
jotura ls-documents --json
jotura read ".md_store/attachments/q3-report.pdf.md"   # read the converted text
```

`search-documents` returns the original `documentPath` (e.g.
`attachments/q3-report.pdf`), not the `.md_store/...` mirror path — so you can
hand it straight back to the user or read the file directly.

## Editing — string-based (preferred)

This is the safest primitive. Strict by default: fails if the old text isn't unique. Mirrors Claude Code's own `Edit` tool semantics.

```sh
# Single targeted change — fails with code 4 if "TODO: reply to alice" appears more than once
jotura edit notes/today.md \
  --replace "TODO: reply to alice" \
  --with "DONE: replied to alice" \
  --if-hash "$HASH"

# Replace every occurrence (use when you mean it)
jotura edit notes/today.md --replace "TODO" --with "DONE" --all

# Replace the Nth occurrence (1-indexed) — use when --replace returns code 4 (Ambiguous)
jotura edit notes/today.md --replace "TODO" --with "DONE" --nth 2
```

When `--replace` fails with exit code 4 (`Ambiguous`), stderr JSON includes `lineHits: [12, 47, 83]`. Use those numbers to pick the right `--nth`, or extend the old-text to include enough surrounding context that it becomes unique.

## Editing — line-based (when string matching doesn't fit)

Get the line numbers from `jotura read --numbered` first. Line numbers are 1-indexed and refer to the **body** of the note (frontmatter is stripped from the count).

```sh
jotura edit notes/today.md --replace-line 12 --with "new content for line 12"
jotura edit notes/today.md --replace-lines 12:15 --with "new\nmulti-line\ncontent"
jotura edit notes/today.md --insert-after 12 --content "new line inserted"
jotura edit notes/today.md --insert-before 1 --content "new first line of body"
jotura edit notes/today.md --delete-lines 12:15
jotura edit notes/today.md --append --content "appended at end"
jotura edit notes/today.md --prepend --content "prepended after frontmatter"
```

`--content` is also readable from stdin: `echo "stuff" | jotura edit ... --insert-after 5`.

## Editing — multi-edit in one call

`jotura edit ... --apply <FILE>` (or `-` for stdin) takes a JSON array of edit ops and applies them **sequentially to the evolving content**. One `--if-hash` precondition covers the whole batch; the final result is committed in a single atomic write.

```sh
cat > /tmp/edits.json <<'JSON'
[
  {"kind": "replace", "old": "TODO: x", "new": "DONE: x"},
  {"kind": "replace", "old": "TODO: y", "new": "DONE: y", "all": true},
  {"kind": "replace-line", "line": 12, "content": "new line 12"},
  {"kind": "insert-after", "line": 20, "content": "after 20"},
  {"kind": "delete-lines", "start": 25, "end": 27},
  {"kind": "append", "content": "end"}
]
JSON

jotura edit notes/today.md --apply /tmp/edits.json --if-hash "$HASH" --json
# or via stdin:
jotura edit notes/today.md --apply - --if-hash "$HASH" --json < /tmp/edits.json
```

Supported `kind` values: `replace`, `replace-line`, `replace-lines`, `insert-before`, `insert-after`, `delete-lines`, `append`, `prepend`. Field names match the matching CLI flag (`new` instead of `with`).

**Important — line numbers shift as ops apply.** Each op observes the state produced by every prior op. If op 0 deletes lines 1–3, then op 1's `line: 5` refers to what was originally line 8. Plan your edits with this in mind, or use string-based replaces which are stable across shifts.

**Atomic on failure.** If any op fails (`NotFound`, `Ambiguous`, `OutOfRange`), the whole batch aborts with **exit code 11** (`MultiEditOpFailed`) and nothing is written. The error names the op that failed:

```json
{
  "code": "MultiEditOpFailed",
  "opIndex": 2,
  "underlyingCode": "Ambiguous",
  "underlying": { "code": "Ambiguous", "data": { "needle": "...", "lineHits": [12, 47] } }
}
```

## Atomic multi-note changes — `jotura batch`

`jotura batch <FILE>` (or `-`) applies a JSON array of file-level operations across multiple notes. Use this when you want several notes updated together — e.g. "retag five notes and rename one folder" — and want all-or-nothing semantics on the most common failure (hash conflict).

```sh
cat > /tmp/ops.json <<'JSON'
[
  {"path": "inbox/a.md", "op": {"kind": "write",      "content": "...", "ifHash": "..."}},
  {"path": "inbox/b.md", "op": {"kind": "edit",       "apply": [{"kind":"replace","old":"x","new":"X"}], "ifHash": "..."}},
  {"path": "inbox/c.md", "op": {"kind": "create",     "parent": "inbox", "name": "c.md"}},
  {"path": "old/d.md",   "op": {"kind": "rename",     "to": "archive/d.md"}},
  {"path": "trash/e.md", "op": {"kind": "delete"}},
  {"path": "inbox/f.md", "op": {"kind": "frontmatter-set", "key": "status", "value": "done"}},
  {"path": "inbox/g.md", "op": {"kind": "tag-add",    "tag": "project-x"}}
]
JSON

jotura batch /tmp/ops.json --diff
```

Always emits JSON to stdout (the global `--json` flag is implied):

```json
{
  "opsApplied": 7,
  "results": [
    {"path": "inbox/a.md", "kind": "write", "newHash": "...", "diff": "..."},
    {"path": "inbox/b.md", "kind": "edit",  "newHash": "...", "diff": "..."},
    ...
  ]
}
```

### Atomicity guarantee

1. **Plan phase**: the CLI reads current content for every path, runs body-modifying ops against it in-memory, and surfaces any `NotFound`/`Ambiguous`/`OutOfRange` errors **before** writing anything. Exit code `11` (`MultiEditOpFailed`) on failure; disk untouched.
2. **Precondition phase**: every `ifHash` is verified against current on-disk state in one pass. Any mismatch produces a `HashConflict` error with **all** conflicting paths and their per-path `currentVsIntended` diffs. Exit code `2`. Disk untouched.
3. **Apply phase**: the batch executes through the existing per-op core API, each op committing in its own SQLite transaction. The plan phase eliminates the common failure modes, but if an unrelated error (filesystem, manifest corruption, AlreadyExists) hits mid-batch, prior ops in that same batch have already committed and will NOT be rolled back.

In practice this means: **safe for "hash conflict" and "op planning errors" (the common cases). Best-effort for unrelated errors mid-write.** Always pass `ifHash` where the user might be editing concurrently.

### Batch op kinds

| `kind` | Required fields | Optional |
|---|---|---|
| `write` | `content` | `full`, `ifHash` |
| `edit` | `apply` (array of edit ops) | `ifHash` |
| `create` | `parent`, `name` | — |
| `rename` | `to` | — |
| `delete` | — | — |
| `frontmatter-set` | `key`, `value` | `ifHash` |
| `frontmatter-delete` | `key` | `ifHash` |
| `tag-add` | `tag` | `ifHash` |
| `tag-remove` | `tag` | `ifHash` |

`--dry-run` and `--diff` work on `batch` just like on single commands.

## Frontmatter

The `edit` command **never touches frontmatter**. Use these dedicated operations instead. Frontmatter changes are isolated so YAML can't accidentally be mangled.

```sh
# Read
jotura frontmatter get notes/today.md          # full YAML block
jotura frontmatter get notes/today.md status   # one key

# Write
jotura frontmatter set notes/today.md status "in-progress"
jotura frontmatter delete notes/today.md draft

# Tag helpers (operate on the `tags:` array)
jotura tag add notes/today.md project-x
jotura tag remove notes/today.md old-tag
jotura tag list notes/today.md
```

The frontmatter editor handles top-level scalars and `tags:` arrays. Complex nested YAML (anchors, multi-line scalars, nested maps) is preserved verbatim but can't be edited from the CLI — for those, ask the user to edit in the desktop.

## Daily notes

```sh
# Print today's daily-note path (auto-creates if missing)
jotura today --json
# → { "path": "daily/2026/05/26.md", "hash": "...", "created": true }

# Print today's body
jotura today --read

# Append a line (also reads from stdin if --append has no value)
jotura today --append "took a 20-minute walk after lunch"

# Operate on a different date
jotura today --date 2026-05-20 --read

# Override the folder (default `daily`)
jotura today --folder Journal
```

Convention: `<folder>/YYYY/MM/YYYY-MM-DD.md`. The CLI's `today` is path computation + auto-create — once you know the path, `edit` and `read` work the same as for any other note.

## Reacting to vault changes

`jotura watch` streams change events as newline-delimited JSON. Useful when scripting against the vault while the desktop or another agent is also editing.

```sh
jotura watch
# {"kind":"Created","path":"inbox/new.md","ts":"2026-05-26T17:50:00Z"}
# {"kind":"Modified","path":"daily/2026/05/26.md","ts":"..."}

# Filter to a glob of paths
jotura watch --paths "daily/**"

# Filter event kinds (C,M,D,R)
jotura watch --kinds C,M

# Exit after the first matching event
jotura watch --once
```

Events are detected by polling the file tree about once per second; renames surface as a Deleted + Created pair.

## File operations

```sh
jotura create inbox "meeting-notes"   # creates inbox/meeting-notes.md (auto-suffixes (2) on collision)
jotura rename old/path.md new/path.md
jotura trash inbox/old.md             # SOFT-DELETE (recoverable, recommended)
jotura delete inbox/old.md            # PERMANENT delete — escape hatch only

# `jotura delete --trash <path>` is an alias for `jotura trash <path>`.
```

For agent use, prefer `jotura trash` — it's reversible. `jotura delete`
permanently removes the file with no recovery path.

## Soft-delete with `trash`

Soft-deletion moves the file into `<vault>/.jotura/trash/`, keeping its
relative path and a small metadata sidecar. The note becomes invisible to
`ls`, `read`, `search`, `quick-open`, and `grep`, but it can be restored at
any time. The desktop app uses the same trash directory.

```sh
# Soft-delete a note
jotura trash inbox/old.md

# List everything in the trash (sorted newest-first)
jotura trash list --json
# → [{ "originalPath": "...", "isFolder": false, "size": 123, "trashedAt": 1716... }]

# Restore at the original path (fails if a note now exists there)
jotura trash restore inbox/old.md

# Restore at a different path
jotura trash restore inbox/old.md --rename-to archive/old.md

# Permanently delete one trashed item
jotura trash purge inbox/old.md

# Permanently delete every trashed item (requires --force)
jotura trash empty --force
```

The original path is the key for `restore` and `purge` — pass the same
logical path the note had before it was trashed. `jotura trash list` shows
those keys.

Restoring into a path that's already occupied returns `InvalidArgs` (exit
code 9) with a message suggesting `--rename-to`. Folders can be trashed and
restored whole.

## Templates

Reusable note bodies live at `.jotura/templates/<name>.md` inside the
vault (plain files, but excluded from `ls`/`search`/`quick-open`/`grep`).

```sh
# List available templates
jotura templates list

# Print a template's body
jotura templates show meeting

# Create a template from stdin
echo "# {{title}}\n\nDate: {{date}}\n" | jotura templates create meeting

# Create a template from an existing note's body
jotura templates create from-current --from inbox/current.md

# Delete a template
jotura templates delete meeting
```

### Using a template

Pass `--template <name>` to `create` or `today` to initialize the new
note's body from a template. Existing notes are never overwritten — the
flag only affects newly-created notes.

```sh
jotura create inbox meeting-notes --template meeting
jotura today --template daily
```

### Template variables

Simple find-replace (no conditionals, loops, or partials). Both the
frontmatter block and the body have variables substituted.

| Variable | Substitution |
|---|---|
| `{{date}}` | today's date, `YYYY-MM-DD` (UTC) |
| `{{datetime}}` | current ISO 8601 timestamp |
| `{{filename}}` | the new note's filename without `.md` |
| `{{title}}` | the filename with `-`/`_` turned to spaces, title-cased |

`{{date}}` always reflects the **current** date, even when `today --date
<past>` is used — that flag controls the path, not the variable.

## Whole-body writes (rare — prefer `edit`)

Only use this when the user genuinely wants to replace a note's entire body. Reading-then-editing with `edit --replace` is almost always better because it preserves any content the user added that you might not have anticipated.

```sh
echo "new body content" | jotura write notes/today.md --if-hash "$HASH"

# --full replaces frontmatter too. Almost never the right move.
echo "---\nfoo: bar\n---\nbody" | jotura write notes/today.md --full --if-hash "$HASH"
```

## Exit codes — handle these by reading the JSON error on stderr

| Code | Name | Recovery |
|---|---|---|
| 0 | success | proceed |
| 2 | `HashConflict` | error includes `currentVsIntended` unified diff; inspect it to decide retry / merge / abandon. For `batch`, comes as a `conflicts: [...]` array of per-path entries |
| 3 | `NotFound` | the string you tried to replace isn't there, or the path is wrong — re-read and check |
| 4 | `Ambiguous` | parse `lineHits` from the JSON error; pick `--nth N` or extend `--replace` text for uniqueness |
| 5 | `OutOfRange` | line number exceeds `lineCount`; re-read with `--numbered` |
| 6 | `NoVault` | ask the user to set `JOTURA_VAULT` or open a vault in the desktop |
| 8 | `PermissionDenied` | surface the message to the user |
| 9 | `InvalidArgs` | bad flag combination; re-check `--help` |
| 11 | `MultiEditOpFailed` | one op in a `--apply` or `batch` failed; check `opIndex` + `underlyingCode` to identify which op and why; the whole batch was aborted with no writes |
| 12 | `SyncNotEnabled` | `sync login`/`sync logout` on a vault without sync configured; only relevant to sync key management |

Errors are emitted as a single-line JSON object on stderr, e.g.:
```
{"code":"Ambiguous","message":"...","needle":"TODO","occurrences":3,"lineHits":[5,12,47]}
```

## What the CLI deliberately does NOT do

Delegate these to the desktop app:

- Create a new vault (any folder works — but the desktop is the usual flow)
- Enable sync / set or rotate the sync password / restore from the server
  (the CLI only manages the local key cache: `sync status|login|logout`)
- Sync push / pull loops
- Daily-notes auto-creation flow (just `create` with a `YYYY-MM-DD.md` name if you need one)
- Task list views (the desktop has UI for this; CLI doesn't expose it)
- Recently-opened note list (UI feature)
- Move-to-OS-trash (the CLI's `trash` keeps soft-deleted notes inside the
  vault so they're available across machines)
- Opening files in external apps

## Common mistakes to avoid

1. **Prefer `jotura` over `sed`/`awk`/direct writes for mutations.** Vault files are plain `.md` and direct edits won't corrupt anything, but they skip the safety rails: no `--if-hash` protection against concurrent desktop/sync edits, no unique-match strictness, no frontmatter preservation guarantees, no atomic write.
2. **Never write into `<vault>/.jotura/`** (except templates via the `templates` commands). It holds the sync database, search index, and trash — the CLI and desktop manage it.
3. **Always use `--if-hash` on writes** of notes the user might be actively editing. Without it, you can silently clobber their desktop work.
4. **Always use `--json` for structured output** when you'll act on the result programmatically. Default output is human-readable.
5. **Don't use `--replace --all` defensively.** Strict mode catches "I thought there was one but there were five" mistakes. Use `--all` only when the user explicitly asked for "every occurrence."
6. **Don't whole-body-write when you mean to edit a section.** `jotura write` replaces the body wholesale. Use `jotura edit --replace` to surgically change one place.
7. **Don't run `jotura sync login` yourself.** It needs the user's sync password. If sync uploads are paused for a missing key, tell the user to run it (or unlock in the desktop) — your reads and edits work fine regardless.

## Shell completion

The CLI ships completion scripts for bash, zsh, fish, elvish, and powershell:

```sh
# zsh — install once
jotura completion zsh > "${fpath[1]}/_jotura"

# bash
jotura completion bash > /usr/local/etc/bash_completion.d/jotura

# fish
jotura completion fish > ~/.config/fish/completions/jotura.fish
```

After install, `jotura <TAB>` completes subcommands and flags. For path arguments, completion delegates to `jotura __complete-paths <prefix>`, which walks the vault's file tree. When no vault is selected the completer returns nothing silently (no error).

## Troubleshooting with `doctor`

`jotura doctor` runs a sequence of health checks — CLI on PATH, vault detected and readable, sync state, document conversion health, desktop app version, etc. Use this first when something feels off:

```sh
jotura doctor --json
```

Each check returns `{ name, status: pass|warn|fail|info, detail }`. Doctor never mutates state.

## Installing this skill in other agents

```sh
jotura skill install claude    # ~/.claude/skills/jotura-vault/SKILL.md
jotura skill install codex     # ~/.codex/AGENTS.md (frontmatter stripped)
jotura skill install gemini    # ~/.gemini/GEMINI.md (frontmatter stripped)
jotura skill install all       # all three
jotura skill list              # show which are installed and up-to-date
jotura skill show              # print canonical SKILL.md to stdout
```

If the target file exists with different content the command prompts before overwriting (or use `--force`).

## Quick reference (memorize this)

```
status / doctor   → vault + sync state / full health
sync status|login|logout → sync key cache management
ls --json         → list notes
search Q --json   → full-text search
search Q --in P   → full-text search scoped to a path prefix
quick-open Q      → filename search
import FILE [--folder F | --as P] [--no-convert]  → add a document + md_store mirror
search-documents Q → search documents (md_store); returns original paths
ls-documents [--folder F]  → list documents + conversion status
regenerate-md-store [P...] [--all] [--stale-only]  → rebuild document mirrors
read P --numbered → read with line numbers
read P --json     → read with hash for --if-hash
edit P --replace OLD --with NEW --if-hash H   → safe targeted edit
edit P --apply FILE --if-hash H               → multi-op edit, sequential, atomic
edit ... --dry-run / --diff                   → preview / unified diff
edit P --replace-line N --with C              → line-based edit
batch FILE [--dry-run] [--diff]               → atomic multi-file ops
frontmatter get/set/delete P [K] [V]          → YAML ops
tag add/remove/list P [T]                     → tags
create PARENT NAME [--template T]              → new note (optionally from template)
rename A B / delete P                          → rename / permanent delete
trash P                                        → soft-delete (recoverable)
trash list / restore P [--rename-to N]         → list / restore trashed
trash purge P / trash empty --force            → permanent removal
templates list/show/create/delete              → manage templates
today [--read] [--append T] [--date D] [--template T] → daily-note helper
backlinks P [--limit N]                        → notes linking TO P
links P [--limit N]                            → links inside P (outgoing)
grep PAT [-i -x -w -c -l -L]                   → regex search across bodies
                [--include G --exclude G]
                [--max-count N]
watch [--paths G] [--kinds CMDR] [--once]      → stream events
completion <shell>                             → emit shell completion
skill install <agent>                          → install this skill
```
