CLI: a command for the whole link graph, not one note at a time

Use case or problem

links and backlinks answer for a single file. There is no command for the vault’s link graph as a whole — obsidian help | grep -i graph matches nothing — so anything that reasons about the vault’s structure has to rebuild the graph one note at a time

On a vault of 5512 files, 1296 of them markdown, that is:

$ obsidian files | grep '\.md$' > list
$ while IFS= read -r f; do obsidian links path="$f" </dev/null; done < list | wc -l
7161

1296 calls, about 1.5 seconds, and 7161 lines that cannot be attributed afterwards: links prints target paths only, with no column naming the file the call was about. The caller has to remember which call produced which run of lines, and a single mis-step silently reassigns every edge after it. The obvious loop is also wrong for a second reason — without </dev/null the CLI consumes the list from stdin and the loop ends after one file, with no error and a plausible-looking result

The app already holds the finished structure. app.metadataCache.resolvedLinks is an object of source path to an object of target path to link count — the same data the graph view draws — and returning it costs one call

Proposed solution

A graph command that emits the whole thing:

$ obsidian graph format=tsv
02. Projects/Tunnel.md	02. Projects/Tunnel/Client and profiles.md	1
02. Projects/Tunnel.md	00. Attachments/diagram.png	2

$ obsidian graph format=json
{"02. Projects/Tunnel.md":{"02. Projects/Tunnel/Client and profiles.md":1}}

Worth having alongside it:

  • unresolved to emit unresolvedLinks in the same shape, so broken targets can be told from real files without a second pass
  • total for the edge count, matching the convention every other listing follows
  • notes to leave attachments out, since an image linked from forty notes is an edge in the index but rarely one in the question being asked

format=tsv matters more than JSON here: the graph is roughly 1 MB of JSON for 1300 notes, and a tab-separated triple per line streams through grep, sort and awk without a parser or a full read into memory

Current workaround (optional)

eval, which reaches the same object in one call and is what the CLI would be exposing:

obsidian eval code='(() => JSON.stringify(app.metadataCache.resolvedLinks))()'

It works, and it is 6 ms against 1.5 s for the loop, but it asks every script that wants the vault’s structure to run JavaScript inside the running app for data the app already has ready

Related feature requests (optional)

  • “Structured JSON Output for Obsidian CLI” (topic 112869)
  • aliases has the same shape of gap on a smaller scale: “CLI: give aliases a format= option, and its verbose output an array of paths” (topic 119160)