Skip to content

feat(obsidian): headless CLI vault ingestion (hindsight-obsidian-sync) - #3179

Merged
benfrank241 merged 6 commits into
mainfrom
feat/obsidian-headless-cli
Aug 4, 2026
Merged

feat(obsidian): headless CLI vault ingestion (hindsight-obsidian-sync)#3179
benfrank241 merged 6 commits into
mainfrom
feat/obsidian-headless-cli

Conversation

@benfrank241

Copy link
Copy Markdown
Member

Closes #3128.

What & why

The Obsidian integration only runs inside the desktop app (isDesktopOnly: true), so a vault hosted on a headless server (kept current on disk by Obsidian Sync) can't be ingested without either running the full Electron app hidden or hand-rolling ingestion against the HTTP API and losing the plugin's carefully-designed behavior. @0xble asked for a supported headless path.

The codebase was already set up for this: SyncEngine is written against the minimal SyncVault/SyncFile interfaces and dependency-injects the vault, sync index, and persist writer — the only Obsidian coupling was requestUrl in the client. So this is a thin second frontend over the same engine, which means a server-synced vault produces identical document ids, scope tags, and prune-ownership as the plugin — the two ingesters never fight or duplicate.

How it's built (3 commits)

  1. refactor: transport seam. Introduce a Transport abstraction so HindsightClient no longer imports obsidian. The plugin injects an obsidianTransport (requestUrl, to escape the renderer CORS sandbox); the CLI injects a fetch transport. One client, one set of request semantics — no divergent copies. Existing client tests pass unchanged (they switch from mocking requestUrl to injecting a fake transport).
  2. feat: the Node frontend (src/node/):
    • fs-vault.ts — filesystem SyncVault (recursive *.md, POSIX-relative paths, ms mtime/ctime, skips dotfolders)
    • fetch-transport.tsfetch-based Transport
    • json-index.ts — sync index persisted to JSON, atomic write; defaults to ~/.hindsight/obsidian/<vault>.json (outside the vault, so Obsidian Sync never propagates it)
    • cli.ts / cli-bin.tshindsight-obsidian-sync reconcile --vault <p> --bank <id> with env fallbacks, --include/--exclude/--prefix-doc-id, and a chokidar --watch mode
    • Packaging: second esbuild target builds dist/cli.js (node, shebang); package.json gains the bin, a files allowlist, and chokidar. The plugin build (main.js) and the release mirror are untouched.
  3. test: real-framework + e2e coverage (see below).

Usage

npm install -g @vectorize-io/hindsight-obsidian
hindsight-obsidian-sync reconcile --vault ~/Vaults/Brain --bank my-vault \
  --api-url https://api.hindsight.vectorize.io --api-token hsk_...
# or live: add --watch

Testing — 90 tests (was 46)

Deterministic units plus real-framework and end-to-end suites:

  • Real chokidar watch (watch.spec.ts) — drives an actual watcher over a temp vault: create → ingest, modify → re-ingest, delete → prune, non-markdown ignored.
  • Real HTTP e2e (e2e-http.spec.ts) — runCli against a live node:http server, nothing mocked: asserts the retain POST (bearer token, document id, scope tags), a real DELETE on prune, and exit-1 when unreachable.
  • Full-stack reconcile over a real temp vault: create/skip/update, mtime-only touch, delete, rename, include/exclude scoping, prefixDocId, prune-ownership, frontmatter tags + created-date timestamp, empty-body skip.
  • Dual-ingester parity — the filesystem and in-memory (plugin) vaults emit byte-identical retain requests.
  • Transport — client error propagation, health(), fetch rejection, cross-transport request parity.

tsc --noEmit clean; both bundles build. The existing test-obsidian-integration CI job (install → tsc → build → test) covers all of it.

Notes

  • ~/.hindsight/obsidian/<vault>.json index location is deliberately out-of-vault; the README documents the shared-scope constraint when running the CLI and plugin against the same bank.
  • @vectorize-io/hindsight-obsidian is already npm-published on release (the release workflow runs npm run build — which now also emits dist/cli.js — and npm ci before publish), so npm install -g gets a working hindsight-obsidian-sync bin with chokidar.

Thanks to @0xble for the well-scoped request and the code reading that made the direction obvious.

…sport seam

Introduce a Transport abstraction so the HTTP client no longer imports
`obsidian` directly. The plugin injects an obsidian-transport (requestUrl,
to escape the renderer CORS sandbox); a headless CLI will inject a
fetch-based transport. This lets both frontends share one client and one set
of request semantics instead of maintaining divergent copies.

No behavior change: existing client tests pass unchanged after switching from
mocking requestUrl to injecting a fake transport (same request shape).
Add a headless second frontend over the shared SyncEngine so a vault can be
ingested into Hindsight from an always-on server with no Obsidian desktop app
(issue #3128). Because it drives the same engine as the plugin, it produces
identical document ids, scope tags, and prune-ownership — the two ingesters
never fight or duplicate.

New Node modules (src/node/):
- fs-vault.ts    — filesystem SyncVault (recursive *.md walk, POSIX-relative
                   paths, ms mtime/ctime, skips dotfolders)
- fetch-transport.ts — fetch-based Transport for the client (no renderer CORS)
- json-index.ts  — sync index persisted to JSON, atomic write; defaults to
                   ~/.hindsight/obsidian/<vault>.json (outside the vault so
                   Obsidian Sync never propagates it)
- cli.ts / cli-bin.ts — `hindsight-obsidian-sync reconcile --vault <p> --bank
                   <id>` with env fallbacks, --include/--exclude/--prefix-doc-id,
                   and a chokidar --watch mode

Packaging: second esbuild target builds dist/cli.js (node, shebang); package.json
gains the bin, a files allowlist, and chokidar. README documents the CLI, the
out-of-vault index, and the shared-scope constraint when running both frontends
against one bank.

Tests (33 new, 79 total): FsVault, json-index, fetch transport, CLI arg parsing
+ watch handlers + a full runCli path (fetch mocked), and a full-stack reconcile
suite over a real temp vault (create/update/skip/delete/rename/exclude/prefix/
prune-ownership) plus a parity check that the filesystem and in-memory (plugin)
vaults emit byte-identical retain requests.
Add higher-fidelity tests beyond the mocked units, and refactor watch mode to
be testable:

- Extract watchVault() from startWatch() so a test can drive a REAL chokidar
  watcher over a temp vault and then close it. New watch.spec.ts asserts
  create/modify/delete on disk flow through to the engine and non-markdown is
  ignored (polling + tight awaitWriteFinish for CI determinism).
- e2e-http.spec.ts runs runCli against a real node:http server — the full
  FsVault → SyncEngine → HindsightClient → fetch → sockets path with nothing
  mocked: asserts the retain POST (bearer token, document id, scope tags) and a
  real DELETE on prune, plus exit-code 1 when the server is unreachable.
- fetch-transport: full-stack HindsightClient error propagation + health()
  true/false, a fetch-rejection case, and a cross-transport parity test proving
  the same call yields an identical request under two transports.
- reconcile: frontmatter tags + created-date timestamp + vault metadata,
  empty-body skip, and includeFolders-only scoping.
- cli: --exclude threaded end-to-end through runCli.

Pin chokidar to ^4.0.3 (bundles its own TS types). 90 tests pass (+11).
… add CLI to docs-site page

- Parity test pinned mtime via utimes but ctime/birthtime can't be set and
  differs across OSes (macOS clamps birthtime to a past mtime, Linux doesn't),
  so the FS vault's created-date tags diverged from the memory vault's on Linux
  CI. Give both notes a frontmatter created: date so the tags come from the note.
- Add a 'Headless / CLI ingestion' section to the public docs-site page
  hindsight-docs/docs-integrations/obsidian.md (mirrors the package README).
@benfrank241
benfrank241 force-pushed the feat/obsidian-headless-cli branch from 843255d to 3afa5d7 Compare August 4, 2026 19:49
@benfrank241
benfrank241 merged commit 93fa0b0 into main Aug 4, 2026
90 checks passed
@benfrank241
benfrank241 deleted the feat/obsidian-headless-cli branch August 4, 2026 21:00
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.

Obsidian integration: support headless/CLI vault ingestion (no desktop app)

1 participant