Skip to content

feat(operations)!: migrate vault operations off Obsidian CLI to direct disk access#58

Merged
AlexMost merged 25 commits into
mainfrom
worktree-migrate-off-obsidian-cli
Jul 17, 2026
Merged

feat(operations)!: migrate vault operations off Obsidian CLI to direct disk access#58
AlexMost merged 25 commits into
mainfrom
worktree-migrate-off-obsidian-cli

Conversation

@AlexMost

Copy link
Copy Markdown
Owner

Summary

Migrates all six vault-operation methods off the Obsidian CLI to direct disk access, so neuro-vault runs fully headless — no obsidian binary, no running Obsidian app. This unblocks headless VPS deployment (Bro v0). Done as a strangler-fig migration: FsVaultProvider first delegated to ObsidianCLIProvider, then each method was flipped to a disk implementation one at a time, and finally the CLI path was deleted entirely.

OpenSpec change: migrate-off-obsidian-cli (archived in this PR under openspec/changes/archive/2026-07-17-migrate-off-obsidian-cli/; new capability spec openspec/specs/headless-vault-operations/).

⚠️ Breaking change (major)

  • The --obsidian-cli config option is removed — launch commands passing it now fail to start (yargs strict).
  • The CLI_NOT_FOUND / CLI_UNAVAILABLE / CLI_TIMEOUT error codes no longer occur (nothing shells out).

What changed

  • New FsVaultProvider implements all 6 VaultProvider methods on disk: createNote (wx write, NOTE_EXISTS, name→folder via .obsidian/app.json), readDaily (resolves today's path from .obsidian/daily-notes.json via a minimal moment-format renderer), setProperty/removeProperty (YAML parseDocument round-trip — body byte-identical, comments preserved, idempotent absent-key remove, last-key strips block), listTags/listProperties (aggregate over the frontmatter scan).
  • Removed ObsidianCLIProvider, the --obsidian-cli flag, the CLI_* error codes, and the CLI error-mapping/write-defenses docs. A grep 'CLI_' src/ sweep test keeps it gone.
  • Docs: ADR-0009 supersedes ADR-0007; cli-write-defenses.mddisk-write-path.md; guide/README swept of CLI prerequisites.

Accepted divergences from the old CLI (documented in ADR-0009)

  • listTags counts frontmatter tags only (not inline #tags) — consistent with query_notes.
  • setProperty no longer registers new property types in .obsidian/types.json.
  • createNote applies no Obsidian templates (callers pass fully-formed content).

Test plan

  • npm test — 748 passing; npm run lint, npx tsc --noEmit, npm run build all clean.
  • Every method has disk tests over temp vaults; a get_vault_overview test runs against a rejecting (dead) CLI.
  • Headless end-to-end smoke: with obsidian off PATH, drove create_noteread_dailyset_propertylist_tagsget_vault_overview against a temp vault via the production factory wiring — all succeeded, disk side-effects verified, no CLI_* leak.

Non-blocking follow-ups are recorded in the change's retrospective.md §5.

Release note: merge to main, then npm run release on main (major bump; the BREAKING CHANGE footer drives the changelog).

🤖 Generated with Claude Code

AlexMost and others added 25 commits July 17, 2026 10:50
Passes the already-constructed VaultReader through IVaultEntryDeps.providerFactory
so FsVaultProvider can receive it. Pure plumbing: no method behavior changes.
FsVaultProvider gains a requireReader() guard for Task 4's disk-direct
listTags/listProperties implementation to use.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
…rovider

Aggregate {name, count} from a vault-relative reader.scan() +
reader.readNotes() frontmatter pass instead of delegating to the CLI.
Revives get_vault_overview in headless mode, since it calls these two
provider methods with no try/catch.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Resolves today's daily-note path from .obsidian/daily-notes.json via
readDailyNotesConfig + formatDailyDate and reads it straight from disk,
removing the last daily-leg delegation to the Obsidian CLI. Missing
config surfaces DAILY_NOTES_NOT_CONFIGURED; missing today-note surfaces
NOT_FOUND with the resolved path.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Writes notes directly to disk (mkdir + writeFile with wx/w flags) instead
of delegating to the Obsidian CLI. Content is written verbatim (no
template expansion — the create_note tool already tells callers to render
templates themselves). name-based paths resolve a folder prefix from
.obsidian/app.json (newFileLocation=folder + newFileFolderPath), matching
Obsidian's own new-note placement. Parent directories are now created
automatically, which the CLI could not do. NOTE_EXISTS error wording is
unchanged for tool-layer behavior parity.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
setProperty/removeProperty now read-mutate-write the frontmatter YAML
block directly via yaml's parseDocument, preserving comments/formatting
of untouched keys and the note body byte-for-byte. Removing the last
key strips the block; removing an absent key is a no-op write. All 6
VaultProvider methods are now disk-direct; the cli field and
ObsidianCLIProvider import are kept unused pending Task 9's removal PR.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
…flag

Delete the dead ObsidianCLIProvider path now that all six VaultProvider
methods are disk-direct: the provider file and its test, the `cli` field on
FsVaultProvider, the `--obsidian-cli` config option, and the
CLI_NOT_FOUND/CLI_UNAVAILABLE/CLI_TIMEOUT error codes. FsVaultProviderOptions
is now standalone with required `vaultRoot` and `reader`, and the server
instructions describe disk-direct runtime requirements instead of CLI
availability.

BREAKING CHANGE: the server no longer shells out to obsidian-cli. The
--obsidian-cli option is removed (launch commands passing it fail to start)
and the CLI_NOT_FOUND/CLI_UNAVAILABLE/CLI_TIMEOUT error codes no longer occur.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Vault operations (create_note, read_daily, set_property, remove_property,
list_tags, list_properties) now read and write the vault directory
directly via FsVaultProvider — no external process, no Obsidian
CLI/runtime dependency. Mints ADR-0009 recording that decision and its
accepted divergences (frontmatter-only tag counts, no types.json
registration, no template expansion), marks ADR-0007 superseded,
rewrites cli-write-defenses.md as disk-write-path.md, and sweeps every
obsidian-cli / --obsidian-cli / CLI_NOT_FOUND / CLI_UNAVAILABLE
reference out of the architecture and user-facing docs.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Remove the duplicated fence-slicing logic that lived inline in
splitFrontmatter and again as a private sliceYamlBody in FsVaultProvider;
both now call the single exported helper. Adds direct unit tests.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
… coverage

Break the monolithic test/operations/fs-vault-provider.test.ts into one file
per method under test/operations/fs-vault-provider/, sharing a _helpers.ts
(makeVault/makeProvider/makeMockGraph/todayBasename/byPath/byName). Expand
edge-case coverage on every method: 22 -> 75 tests.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
- set_property/remove_property reject ambiguous names with
  AMBIGUOUS_MATCH via a resolver shared with edit_note (was: silent
  alphabetically-first write)
- create_note name traversal ('../x') fails INVALID_ARGUMENT instead
  of an uncoded error
- read_daily reads daily-notes.json once (provider-side only)
- drop the dead vaultProviderFactory hook from the operations module
- rewrite the stale CLI rationale comment in daily-notes-config
- fill the headless-vault-operations spec Purpose; add the ambiguity
  scenario
- pin edge cases: YYYYY / unclosed '[' daily formats, yaml
  trailing-comment association on property writes

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…review 2)

- readDaily: normalize the resolved daily-note path so a
  folder/format that escapes the vault (e.g. folder '../outside')
  is refused with DAILY_NOTES_NOT_CONFIGURED instead of reading
  outside the vault [P1 security]
- setProperty/removeProperty: map write failures to WRITE_FAILED
  (new contract code) instead of leaking a bare fs error
- createNote: wrap mkdir so a directory-creation failure (parent
  path is a file) surfaces as CREATE_FAILED, not raw ENOTDIR/EEXIST
- editFrontmatter: reject syntactically-valid but non-map YAML roots
  (scalar/sequence frontmatter) with READ_FAILED before doc.set()
  throws a plain Error
- formatDailyDate: reject long year runs (YYYYYY, YYYYYYYY) instead
  of silently rendering 202626 / 20262026

Adds regression tests for each and spec scenarios for the traversal
guard and the ambiguity behavior.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@AlexMost
AlexMost merged commit 43d0581 into main Jul 17, 2026
2 checks passed
@AlexMost
AlexMost deleted the worktree-migrate-off-obsidian-cli branch July 17, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant