Reading and searching

Reference for the Jotura CLI commands that only read: read, ls, quick-open, search, grep, backlinks, links, and the two document commands.

Every command on this page reads the vault and changes none of its notes. The one thing any of them writes is the keyword search cache under .jotura/cli-search/, described under search. There is no unlock step, because a Jotura vault is an ordinary folder of Markdown files, so jotura read, jotura search, and the rest work the moment a vault is resolved. If you have not set one up yet, start with Install and setup.

Two commands here matter more than the others. jotura read --json is the source of the content hash that every safe edit depends on, so it is the first half of the loop described in Editing notes. jotura search is the only command with an optional prerequisite, because its semantic and smart modes read an index that the desktop app builds.

Flags that apply everywhere

Three of these flags, --vault, --json, and --verbose, are declared globally, so clap accepts them before or after the subcommand. jotura --json read note.md and jotura read note.md --json are the same command.

Flag Type Default What it does
--vault <VAULT> path none Use this vault for this invocation. Resolution order is --vault, then $JOTURA_VAULT, then the desktop app’s last opened vault.
--json boolean false Emit structured JSON on stdout. Every command on this page respects it.
-v, --verbose boolean false Log progress to stderr. In practice this prints one line naming the resolved vault.
-h, --help boolean none Print help for the command.
-V, --version boolean none Print the CLI version. Top level only.

Errors are always a single line of JSON on stderr whether or not you passed --json. Full details are in Exit codes and JSON output.

One caveat about -v: it is consumed by --verbose at the global level, which is why jotura grep has no GNU-style invert-match flag. See the grep section below.

Note that --json is universal on this page but not across the whole CLI. Roughly half the commands, all of them mutating ones, emit JSON whether or not you ask for it.

What the CLI can see

Every command on this page except read builds its candidate list from the same vault walk, and that walk applies two filters. Understanding them explains most cases of “my file is not in the results”.

The first filter is the dot prefix. Any name beginning with a dot is skipped at every level of the walk, so .jotura/, Obsidian’s .obsidian/ and .trash/, the legacy .md_store/ folder, and ordinary dotfiles never appear.

The second filter is the file extension. A file is only visible when its extension is on one of two lists, and the check is case-insensitive.

List Extensions
Text md, markdown, mdc, txt, rst, org, tex, json, yaml, yml, toml, csv, tsv, xml, html, htm, css, scss, sass, js, mjs, ts, tsx, jsx, py, rb, rs, go, java, kt, swift, c, cpp, h, hpp, sh, bash, zsh, fish, ini, conf, env, sql, log, ps1, bat, canvas, base
Binary pdf, docx, doc, xlsx, xls, pptx, ppt, odt, ods, odp, rtf, epub, png, jpg, jpeg, gif, webp, svg, bmp, ico, heic, heif, avif, tiff, tif, mp3, m4a, wav, ogg, flac, mp4, mov, webm, mkv, cbz, cbr

Obsidian’s .canvas and .base files are JSON, so they sit on the text list and are read, indexed, and edited as plain text.

A file with no extension at all, such as README or Makefile, is on neither list and is therefore invisible. So is any extension the lists do not name, such as .zip. This is the single most surprising thing about the reading commands, and it applies to all of them:

Command Effect of the extension filter
ls Folders are always listed. Files that fail the check are omitted with no warning.
search An excluded file can never be a result, in any mode.
quick-open An excluded file can never be a result.
grep Narrower still: only the text list is scanned, so PDFs and Word files are never grepped.
backlinks Excluded files are neither scanned for links nor available as resolution targets.
links The source file is read directly and is exempt, but excluded files can never be resolution targets.
ls-documents, search-documents An excluded file is not a document as far as these commands are concerned.

read is the exception. It opens the path you name, so it will happily print a README that no other command can see. A file on the binary list, such as a PDF or an .mp4, is not printed: read describes it instead, as shown under read below.

How path arguments are handled

The commands here do not treat their path arguments identically. There are three levels of strictness, and mixing them up leads to expecting an error that never arrives.

Argument Handling
read <PATH> Full validation: normalize, reject bad components, then confine to the vault.
links <PATH> Normalize, then confine to the vault. No component rejection.
backlinks <PATH> Normalize only. The path is never opened, so nothing is checked.
ls --folder, ls-documents --folder, search --in Normalize, then use the value as a plain path-prefix filter. Nothing is rejected.

Normalization is the shared part. Backslashes become forward slashes, so notes\today.md and notes/today.md are the same argument. Leading ./ is stripped, leading and trailing slashes are removed, repeated slashes collapse, and the result is put into Unicode NFC form, so a decomposed filename argument still matches a composed path on disk.

Normalization does not remove .. segments. Only read rejects them:

Situation Exit code Message
Empty path after normalization 9 path is empty
Path contains a . or .. component 9 path must not contain '.' or '..' components
Path targets .jotura/, other than the templates subtree 9 path targets the vault data dir
Path resolves outside the vault, including through a symlink or a Windows junction 8 path resolves outside the vault
File does not exist 3 NotFound

links applies only the last two rows: a source outside the vault exits 8 and a missing source exits 3, but a .. component or a .jotura/ target is not rejected as such.

For the prefix filters there is no error at all. A --folder or --in value that matches nothing, including a nonsense value like ../secrets, produces an empty result and exit code 0. There is no NotFound for a folder that does not exist. The same is true of backlinks, which returns no hits and exits 0 for a target that is not a real path.

The desktop app enforces read-level validation through the same shared code.

read

Print a note’s body to stdout, or return it as JSON along with its hash.

jotura read [OPTIONS] <PATH>
Argument or flag Type Default What it does
<PATH> string required The vault-relative path to read, or a bare note name.
--numbered boolean false Prefix every line with N: , counting from 1.
--with-frontmatter boolean false Include the YAML frontmatter block in the output.

A bare name with no slash or extension resolves like a wikilink to the unique note with that filename; several matches exit 4 with candidates, so prefer the path when you know it. Anything containing a slash, ending in an extension, or existing on disk is taken as a path and never as a name, so an existing Plan.md at the vault root always wins over a nested one. Matching is exact on the filename stem apart from case; there is no fuzzy matching, because a fuzzy hit on an edit path is a write to the wrong note. A resolved name adds resolvedFrom to the JSON payload. jotura edit accepts the same form.

The plain unnumbered form is byte-faithful. Jotura prints the content as it is and adds a trailing newline only when the file does not already end in one, so reading a note never introduces a phantom newline.

jotura read projects/roadmap.md
jotura read projects/roadmap.md --numbered
jotura read projects/roadmap.md --with-frontmatter --numbered

--numbered gives up that fidelity, because it splits the text into lines and prints each one. CRLF line endings come out as LF, and a file that did not end in a newline gains one. Use the plain form when you care about the exact bytes.

Passing both flags numbers the whole file including the frontmatter lines, because the numbering is applied to whatever text was selected for output.

Both flags are ignored in --json mode. JSON always separates the frontmatter into its own field and always returns unnumbered text, so there is nothing left for either flag to change.

jotura read projects/roadmap.md --json
{
  "path": "projects/roadmap.md",
  "hash": "9f2c...",
  "frontmatter": "---\ntitle: Roadmap\n---\n",
  "body": "# Roadmap\n...",
  "lineCount": 42,
  "share": { "shareId": "...", "name": "Team notes", "role": "editor" }
}

Four details in that payload are easy to get wrong.

  • hash is the value you pass back as --if-hash on a write, which is why you read in JSON mode before an edit.
  • frontmatter is null when the note has no YAML block, and it is also null whenever the filename does not end in .md or .markdown. The test is a case-insensitive suffix check, nothing more, so a .txt file that starts with a YAML block is treated as all body. That same test is why the frontmatter and tag commands refuse to operate on other files. See Frontmatter and tags.
  • lineCount counts the lines of the body, not of the whole file. A note with frontmatter has more physical lines than lineCount reports.
  • share appears only when the path routes into a mounted share, and it is the one field on this page that depends on a paid Sync subscription, priced on the pricing page.

How share mounts behave from the command line is covered in Sync and shares.

A path whose extension is on the binary list is never dumped to the terminal. Text mode prints one line naming the MIME type, the size, and the file’s absolute path, with a hint to open it in a tool that reads the format. JSON mode returns { "path", "absolutePath", "kind", "mime", "bytes" }, where kind is image or binary. There is no hash field, because binary files are not edited through the CLI.

ls

List the folders and visible files in the vault.

jotura ls [OPTIONS]
Flag Type Default What it does
--folder <FOLDER> string none, meaning the whole vault Restrict the listing to a subfolder.

The folder value is normalized and then used as a <folder>/ prefix filter, so --folder projects, --folder projects/, and --folder projects\ all behave the same. A value that normalizes to an empty string applies no filter at all.

jotura ls
jotura ls --folder projects
jotura ls --json

The text form prints one path per line. The JSON form is an array sorted by path:

[
  { "path": "projects", "isFolder": true, "size": 0, "mtime": 1724899200000 },
  { "path": "projects/roadmap.md", "isFolder": false, "size": 4210, "mtime": 1724899200000,
    "shareId": "...", "shareRole": "editor" }
]

Folders are included as entries with size set to 0. mtime is milliseconds since the Unix epoch, and it falls back to 0 when the filesystem does not report a modification time. shareId and shareRole are omitted for paths outside a share mount, and they are only ever populated in JSON mode, because the plain-text listing has no column for them.

Both filters from “What the CLI can see” apply here. Folders are listed whatever they are called, as long as the name does not start with a dot, but a file is listed only when its extension is on one of the two lists. A vault containing alpha.md, notes/beta.md, backup.zip, and README lists three entries: alpha.md, notes, and notes/beta.md.

quick-open

Fuzzy match against filenames and frontmatter aliases. This is the CLI equivalent of the desktop app’s quick-open palette, described in Keyboard shortcuts.

jotura quick-open [OPTIONS] <QUERY>
Argument or flag Type Default What it does
<QUERY> string required The fuzzy query, matched against filenames.
--limit <LIMIT> integer 20 Maximum number of results. A limit of 0 is treated as 1.
jotura quick-open roadmap
jotura quick-open roadmap --limit 5 --json

Matching runs against the full filename including its extension and against every alias a note lists under aliases in its frontmatter, not the folder path and not the body, and it builds no index. Results are ordered by descending fuzzy score, then by lowercased filename, then by full path, so ties are stable between runs. The text form prints path, a tab, then the raw integer score, followed by a tab and the alias when the hit came through one. JSON is an array of { "path", "score", "alias" }, where alias holds the alias that matched, or null for a filename match.

An empty query is not an error. It matches every visible file with a score of 0, so the tiebreakers decide the whole ordering and you get an alphabetical listing by filename, truncated by the limit.

Full-text and meaning-based search across the vault.

jotura search [OPTIONS] <QUERY>
Argument or flag Type Default What it does
<QUERY> string required The query text. An empty or whitespace-only query returns an empty result set rather than an error.
--limit <LIMIT> integer 20 Maximum number of results. Keyword mode passes the value through, treating 0 as 1. Semantic and smart modes clamp it to the range 1 to 100.
--in <SCOPE> string none Restrict results to paths under this prefix, for example projects/. Keyword mode only.
--mode <MODE> keyword, semantic, or smart keyword Which search strategy to use.
--no-cache boolean false Build a throwaway index for this one search instead of using the cached keyword index. Keyword mode only; with a semantic mode it exits 9.

The three modes

Mode How it works Prerequisite
keyword Ranked full-text search, plus a fuzzy filename pass. None. Works on any vault.
semantic Embedding similarity. One result per note, and the best-matching chunk within that note wins. The desktop app’s semantic index.
smart Runs keyword and semantic together and fuses the two rankings by reciprocal rank. The desktop app’s semantic index.
jotura search "release checklist"
jotura search "release checklist" --limit 50 --in projects/
jotura search "how do we handle refunds" --mode smart --json

What keyword mode matches

Keyword search runs two passes and concatenates them.

The first pass fuzzy-matches the query against the filename stem, which is the filename with its last extension removed. So the query roadmap matches the file projects/roadmap.md through its stem roadmap. This is the one place where search and quick-open differ: quick-open matches the whole filename, extension included.

That pass is capped at the smaller of your --limit and 20. With --limit 5 the filename pass can return at most 5 hits; with --limit 50 it can return at most 20 and the body pass fills the other 30. Filename hits come back with an empty snippet.

Every visible file takes part in the filename pass, including binary documents. A PDF cannot be read as text, so it is never in the body index, but jotura search invoice can still return attachments/invoice-2026.pdf with an empty snippet, from plain keyword mode, with no document command involved.

The second pass queries the body index and fills the remaining slots, skipping any note the first pass already returned. It is case-insensitive and requires whole words. Multiple terms are combined with AND, so release checklist returns only notes containing both words. Wrapping the query in double quotes switches to a phrase query and skips the filename pass entirely, so jotura search '"release checklist"' matches the two words adjacent and in that order.

Keyword results in JSON are { "path", "title", "snippet", "score" }, where title is the filename stem. The text form is path, tab, score to three decimal places, tab, snippet.

Scoping with –in

--in filters candidates by path prefix, and a scoped search always builds a throwaway index rather than using the cache. Converted document text needs no special route: each document’s mirror is an ordinary sibling note such as report.pdf.md, so plain keyword search already finds it. search-documents, documented below, searches only those mirrors and names the original document.

Passing --in together with --mode semantic or --mode smart is rejected with exit code 9 and the message --in is only supported with --mode keyword.

Keyword performance

The CLI keeps its own keyword index at .jotura/cli-search/, separate from the desktop app’s index. Each unscoped search checks every file’s modification time and size, re-indexes only the files that changed, and then queries. The cache is created only in a vault that already has a .jotura/ folder, so an Obsidian vault opened read-only gets nothing written into it: there, and with --in or --no-cache, each search builds a throwaway in-memory index over the whole vault instead, and the cost is paid per call. Use jotura grep when you want a regex sweep, or one search with a larger --limit when you want more results.

Semantic and smart prerequisites

The CLI is query-only. It embeds your query, reads the vector index, and never writes to it. Building and refreshing that index is the desktop app’s job, which has two consequences worth planning around.

First, semantic search must be turned on in the desktop app before the CLI can use it. Open Settings, then Semantic search, then Enable, and pick a provider: OpenAI embeddings under your own API key, or a local model on the device. The app builds the index with that provider, and the index records which one. The CLI embeds your query with the same provider, so an index built with OpenAI needs an OpenAI API key on the machine running the CLI: OPENAI_API_KEY in the environment wins, otherwise the key the desktop app saved as openai.json in <config>/jotura/ (%LOCALAPPDATA%\jotura\ on Windows) is used. In a vault that syncs, the desktop app fills that file from the encrypted copy it keeps in the vault, so a second machine that has opened the vault in the app with its sync key cached already has the key; the CLI itself never reads the vault copy. With the local provider the feature runs entirely on your device; with OpenAI each query string is sent to OpenAI. Both are covered in Search and Settings reference.

Second, semantic results are only as fresh as the app’s last indexing pass. A note you just wrote with jotura write is immediately findable by keyword and grep, but it is not semantically searchable until the desktop app has indexed it.

Any missing prerequisite exits with code 13, and the message names the specific one:

Missing prerequisite What the message says
<vault>/.jotura/vectors.db does not exist no semantic index for this vault (.jotura/vectors.db missing)
The index exists but the feature is switched off semantic search is disabled for this vault
No config directory could be resolved for the model no config directory for the embedding model
The embedding model files are absent embedding model missing at <path>
The ONNX runtime cannot be found. macOS only, and only for a standalone CLI build ONNX Runtime dylib not found followed by which lookups failed
The runtime is present but the engine fails to load embedding engine failed to load: <reason>
The index uses OpenAI embeddings and no key is available semantic index uses OpenAI embeddings; set OPENAI_API_KEY or add the key in the desktop app (Settings -> Semantic search)
The index uses OpenAI embeddings and OpenAI rejected the key OpenAI rejected the API key; update it in the desktop app or OPENAI_API_KEY

The first six messages contain the same hint pointing you at the desktop app’s setting, but two of them do not end with it: the missing-model message appends to download it, and the ONNX message appends an alternative involving ORT_DYLIB_PATH. The two key messages have their own wording and never mention the model directory, so a script that matches on a suffix will miss some of them; match on the exit code or on a substring. A network failure or rate limit while embedding the query against OpenAI falls outside this table and exits 1 with the response text, key redacted.

Semantic and smart results have a different shape from keyword results. JSON is { "path", "headingPath", "snippet", "score" }, with headingPath in place of keyword mode’s title. The text form is again path, tab, score to three decimal places, tab, snippet. A client that parses both modes has to branch on which field is present.

Three environment variables affect semantic search. OPENAI_API_KEY supplies the key for an OpenAI index and takes precedence over the key saved by the desktop app. JOTURA_MODEL_DIR overrides where the CLI looks for the local embedding model, and an empty value falls back to the default location. On macOS, ORT_DYLIB_PATH points a standalone CLI at a libonnxruntime.dylib. The CLI bundled inside the desktop app resolves that library itself and sets the variable for you. The model directory and the ONNX runtime only matter for local indexes.

grep

Regex search across the text content of the vault.

jotura grep [OPTIONS] <PATTERN>
Argument or flag Type Default What it does
<PATTERN> string required A regular expression.
-i, --ignore-case boolean false Match case-insensitively.
-x, --line-regexp boolean false Require the pattern to match the entire line.
-w, --word-regexp boolean false Require the match to fall on word boundaries.
-c, --count boolean false Print one match count per file instead of the matching lines.
--max-count <N> integer none Stop after N matching lines in each file. There is no short form.
--include <GLOB> string none Only search paths matching this glob.
--exclude <GLOB> string none Skip paths matching this glob.
-l, --files-with-matches boolean false Print only the paths that contained a match.
-L, --files-without-match boolean false Print only the paths that contained no match.
jotura grep "TODO"
jotura grep "^## " --include "projects/*.md"
jotura grep "api[_-]key" -i -l
jotura grep "FIXME" --exclude "archive/*" --max-count 3 --json

Output modes and their precedence

Four output modes exist and exactly one wins. The precedence is -L first, then -l, then -c, then the default line listing. So -L -l -c behaves as plain -L, and combining flags never merges their output.

Mode Text output JSON output
Default path:line:line_text An array of hits with path, line, lineText, and matchedText
-c path:count An array of { "path", "count" }
-l One path per line An array of path strings
-L One path per line An array of path strings

-c and -l list only files with at least one match. -L lists only files with none.

What grep scans, and what it skips

Only files whose extension is on the text list from “What the CLI can see” are scanned. Binary documents such as PDFs and Word files are on the binary list, so grep never opens them. Use jotura search-documents for those. Extensionless files are on neither list and are skipped as well.

Dot-prefixed entries are skipped, so .jotura/ stays out of the results. A document’s converted-text sibling, such as report.pdf.md, is an ordinary note and is scanned like any other. A file that is not valid UTF-8 text is skipped silently rather than failing the whole scan, because a plaintext vault can be written to by any tool on the machine.

Results are emitted in sorted path order so that output is stable between runs. At most one hit is recorded per line, which means --max-count 3 caps a file at three matching lines, not at three total matches. The same cap applies to -c, so a count reported under --max-count 3 never exceeds 3.

Both globs are matched against the whole vault-relative path, as in --include "projects/*.md". An invalid regex and an invalid glob both exit with code 9 and a message naming which one failed.

There is no invert-match flag

GNU grep’s -v is taken by Jotura’s global --verbose, and no long --invert-match was added to compensate. -L, which lists the files containing no match at all, is the closest available behavior. There is no way to list the lines that do not match.

tasks

jotura tasks scans every markdown note in the vault for checklist lines and groups them the way the desktop Tasks pane does.

jotura tasks [OPTIONS]
Flag Type Default What it does
--folder <FOLDER> string the daily folder jotura today would pick The daily-notes folder used to date tasks written in daily notes.
--window-days <N> integer 30 Only include tasks from notes touched or dated within the last N days.
--all flag off Ignore the window and list every task in the vault. Conflicts with --window-days.
--open flag off Only unchecked tasks. Conflicts with --done.
--done flag off Only completed tasks.
--section <S> string none One group only: overdue, today, upcoming, no-date, or done. Anything else exits 9.

A checklist line is - [ ], - [x], or the same with *, +, or an ordered-list marker. The extra task states the editor renders ([/], [-], [>], [?]) are not tasks here, the same rule the desktop Tasks pane follows. A task’s effective date is the @due(YYYY-MM-DD) marker in its text if there is one, otherwise the date of the daily note it lives in. The window keeps a task when its effective date is in the future, when its daily-note date falls inside the window, or when its file was modified inside it, which is what stops a note you have not touched in months from filling the list.

The text form is one tab-separated line per task: path, line number, [ ] or [x], the task text as written, then the effective date or an empty field.

{ "window": { "days": 30, "today": "2026-09-06" },
  "sections": {
    "overdue": [ { "path": "projects/acme.md", "line": 2, "checked": false,
                   "text": "late @due(2000-01-01)", "dueDate": "2000-01-01",
                   "dailyDate": null, "effectiveDate": "2000-01-01" } ],
    "today": [], "upcoming": [], "noDate": [], "done": [] } }

Every section key is always present, empty when nothing matched. window is null under --all. Paths are vault-relative. The desktop side of the same data is described in Daily notes, tasks, and templates.

These two commands are the reading half of the vault’s link graph. backlinks finds every note that points at a target, and links lists what a single note points at.

jotura backlinks [OPTIONS] <PATH>
jotura links [OPTIONS] <PATH>
Argument or flag Type Default What it does
<PATH> string required For backlinks, the link target to find references to. For links, the source note to read.
--limit <LIMIT> integer none, meaning unlimited Cap the number of results.
jotura backlinks projects/roadmap.md
jotura backlinks projects/roadmap --limit 10 --json
jotura links projects/roadmap.md --json

Two forms are recognized. Wikilinks are [[target]], [[target|display text]], and the embed form ![[target]]. Markdown links are [display text](target), and they only count when the target carries an extension from either list in “What the CLI can see” after any anchor is stripped, and is not a URL. A link to https://example.com is therefore ignored, while a Markdown link pointing at a PDF counts. A percent-encoded Markdown target such as [text](My%20Note.md) is decoded before matching.

Anchors are stripped before matching, so [[roadmap#milestones]], [[roadmap#^abc123]], and [Q3](roadmap.md#milestones) all resolve to the note roadmap.md. A link that is only an anchor, such as [[#milestones]], is left with an empty target and is not reported by either command.

How targets resolve

Targets resolve the way Obsidian resolves them, through the same shared resolver the desktop app uses, so the two surfaces always agree on what a link points at.

  1. Matching is case-insensitive throughout. [[roadmap]] finds Roadmap.md.
  2. A target with a slash is matched as a path from the vault root. The .md extension is optional, so projects/roadmap and projects/roadmap.md both resolve to the note at projects/roadmap.md.
  3. A bare name matches the note with the shortest path that ends in that name, so [[roadmap]] resolves to projects/roadmap.md when there is no roadmap.md nearer the root. Notes sharing a filename at equal depth all match, which is why resolved is an array rather than a single value.
  4. A name that matches no filename is checked against every note’s frontmatter aliases, so [[The Plan]] resolves to a note whose frontmatter lists aliases: [The Plan].

Resolution by frontmatter title is still not implemented. A link written as the note’s title rather than its filename or one of its aliases will not resolve.

Embeds of non-markdown files, such as ![[diagram.png]], resolve by filename anywhere in the vault and through the Obsidian attachment folder named by attachmentFolderPath in .obsidian/app.json. They are reported by links but are never backlink sources.

What each command scans

The two commands differ in a way that explains why one is cheap and the other is not.

backlinks walks the whole vault to find candidate sources, and it scans only files whose path ends in a lowercase .md and that are no larger than 25 MB. That filter is case-sensitive, unlike the .md test used by read, so a note saved as NOTES.MD is never scanned for outgoing links by backlinks. It also records at most one hit per line, so a line containing [[roadmap]] twice produces a single hit. That matters when you set --limit, which counts hits.

links walks the vault only to build the set of resolution targets, then reads the single file you named. There is no extension test on that file at all, so jotura links notes/data.txt works and reports the links inside a plain text file.

Neither command keeps a persistent index. The result is always current at the cost of a walk per invocation. Dot-prefixed paths are excluded from that walk.

Output

backlinks returns a target field holding the normalized path you asked about, plus a hits array. Each hit carries path, line counting from 1, snippet holding the whole matching line, and linkKind, which is either wiki or markdown. The text form is path:line, a tab, then the snippet.

{ "target": "projects/roadmap.md",
  "hits": [ { "path": "daily/2026-08-29.md", "line": 7,
              "snippet": "see [[roadmap]] for context", "linkKind": "wiki" } ] }

links returns a source field plus a links array, where each entry carries line, snippet, target as written in the note, linkKind, and resolved, which is the array of vault paths the target resolves to. An empty resolved array means a dead link. A resolved entry also carries absolutePath, the absolute filesystem path of its first resolved target, so an agent can hand it straight to its own file tools. The text form is source:line, a tab, the target, a tab, then -> followed by the comma-separated resolved paths, or the literal (unresolved) when nothing matched.

{ "source": "projects/roadmap.md",
  "links": [ { "line": 3, "snippet": "next: [[q3-plan]]", "target": "q3-plan",
               "linkKind": "wiki", "resolved": ["projects/q3-plan.md"] } ] }

The two commands fail differently on a missing path. jotura links opens the source note, so a path that does not exist exits with code 3. jotura backlinks never opens its target, so asking for backlinks to a note that does not exist returns no hits and exits 0.

search-documents and ls-documents

Jotura stores imported documents such as PDFs and Word files as-is, and generates a searchable Markdown mirror for each one as a sibling note, so report.pdf gets report.pdf.md beside it. These two commands work from the document’s side of that pair. The desktop side is described in Documents, and importing is covered in Documents from the CLI.

jotura search-documents [OPTIONS] <QUERY>
jotura ls-documents [OPTIONS]
Command Argument or flag Type Default What it does
search-documents <QUERY> string required The query text.
search-documents --limit <LIMIT> integer 20 Maximum number of results.
ls-documents --folder <FOLDER> string none Restrict the listing to a subfolder.
jotura search-documents "invoice terms"
jotura search-documents "invoice terms" --limit 5 --json
jotura ls-documents --folder attachments --json

Both commands consider a file a document only when its extension is one of pdf, docx, pptx, xls, xlsx, epub, msg, rtf, odt, odp, or ods. The old binary Office formats doc and ppt are stored without a mirror, because the bundled converter cannot read them. Images, audio, video, and archives are stored without a mirror, because converting them would only scrape metadata. One inconsistency is worth knowing: msg is on the document list but not on the visible-extension list, so Outlook message files never reach either command.

search-documents builds a throwaway in-memory index over the Markdown mirrors on every invocation, so call it once with a useful --limit rather than in a loop. It then maps each hit back to the original document, so the results name the original file in your vault. JSON entries are { "documentPath", "mdStorePath", "title", "snippet", "score" }, where mdStorePath despite its name holds the sibling note’s path. The text form is documentPath, tab, score, tab, snippet. A hit whose mirror path cannot be mapped back to an original document is dropped from the results without a warning.

ls-documents lists eligible documents whether or not a mirror exists for them. The text form is path, tab, then the conversion status as true or false. JSON entries carry path, size, hasMdStore, and conversionOk. The two booleans differ: hasMdStore says the mirror file exists, while conversionOk says the conversion recorded in that mirror succeeded. A stub mirror written when the converter was unavailable has hasMdStore true and conversionOk false, and it heals on a later jotura regenerate-md-store --stale-only.

Exit codes you will see here

Reading commands use a narrow slice of the full table.

Code Symbol When it happens on this page
0 success Including a search or grep that matched nothing, backlinks against a nonexistent target, and a --folder or --in value that matches nothing.
1 Error Unclassified I/O or serialization failure.
3 NotFound read or links given a path that does not exist, or a bare name no note matches.
4 Ambiguous A bare name given to read that several notes match; the error carries candidates.
6 NoVault No vault resolved, or the resolved path is missing or is not a directory.
8 PermissionDenied A filesystem permission error, or a read or links path resolving outside the vault.
9 InvalidArgs An invalid regex or glob for grep, a path component rejected by read, or --in combined with a semantic mode.
13 SemanticUnavailable --mode semantic or --mode smart with a prerequisite missing, including no usable API key for an OpenAI index.

One collision is worth knowing about. Clap’s own usage errors, such as a missing required argument or an unknown flag, exit with code 2, which is also Jotura’s HashConflict code. Those errors print human-readable text on stderr rather than the JSON envelope. Nothing on this page can produce a genuine hash conflict, so a script that treats exit 2 as a conflict should confirm that stderr parses as JSON before drawing that conclusion. The full table lives in Exit codes and JSON output.

Where to go next

Reading is half of the safe-edit loop. Editing notes covers the write side, including how the hash from read --json becomes --if-hash. Frontmatter and tags covers the YAML block that read splits out. Watching and diagnostics covers jotura watch and jotura doctor for when search results do not look right.