Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
feeb9f3
feat(assets): PDF text extraction via unpdf
claude Jul 20, 2026
df3b95e
style: fix prettier formatting in asset-operations tests
claude Jul 20, 2026
94167e7
fix(review): update feature surface docs for PDF text extraction
aliasunder Jul 20, 2026
6b65f94
style: remove redundant typeof guard and add Buffer conversion comment
aliasunder Jul 20, 2026
0cb5ba6
test: exact error assertions + corrupt PDF coverage gap
aliasunder Jul 20, 2026
10bbce0
fix: add MAX_ASSET_BYTES and MAX_IMAGE_OUTPUT_BYTES to config tables
aliasunder Jul 20, 2026
ad128b0
test: collapse decomposed PDF assertions into single toEqual
aliasunder Jul 20, 2026
79d0674
feat(assets): structured PDF extraction via extractTextItems
aliasunder Jul 20, 2026
e2a0b8c
style: readability pass — let-justification comment, null-title comme…
aliasunder Jul 20, 2026
d00a637
test: exact text-cap assertion + trailing code fence coverage gap
aliasunder Jul 20, 2026
c6ea2ac
test: collapse all decomposed assertions in asset handler tests
aliasunder Jul 20, 2026
dcc675a
style: extract roundFontSize helper, decompose nested composition, re…
aliasunder Jul 20, 2026
437f751
style: add blank lines between guard-continue blocks in groupIntoLines
aliasunder Jul 20, 2026
2e01bb5
style: consistent blank-line spacing between logical steps
aliasunder Jul 20, 2026
bb87a27
fix: add missing raw? param to vault_read_asset ARCHITECTURE table
aliasunder Jul 20, 2026
d5f3d4a
style: simplify groupIntoLines and buildHeadingLevels
aliasunder Jul 20, 2026
7d0f2c6
style: clarify groupIntoLines with inline comments
aliasunder Jul 20, 2026
5205955
style: add inline comments across PDF reconstruction logic
aliasunder Jul 20, 2026
8a7176e
style: document the 2px threshold in groupIntoLines
aliasunder Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ createTestIndex()` at the top of each test. `beforeEach` is only
fragment is genuinely under test (one branch among many, or an
excerpt of large output).
- Never decompose: `toHaveLength(1)` + index property checks is
weaker than `expect(results.map(r => r.path)).toEqual(["foo"])` —
weaker than `expect(results.map(result => result.path)).toEqual(["foo"])` —
the decomposed form misses extra items, ordering, and unexpected
properties.
- Deterministic error messages get exact assertions — if the message
Expand Down
5 changes: 3 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,16 @@ Link queries use a `links` table populated during indexing:

| Tool | Input | Annotation |
| ------------------- | ------------------------------ | ------------ |
| `vault_read_asset` | `path` | readOnlyHint |
| `vault_read_asset` | `path, raw?` | readOnlyHint |
| `vault_list_assets` | `folder?, extensions?, limit?` | readOnlyHint |

`vault_read_asset` reads non-markdown vault files, dispatching on extension to the most useful representation per type:

1. **Images** (`.png`/`.jpg`/`.jpeg`/`.gif`/`.webp`) return an MCP `image` content block plus a one-line metadata text block. A shared fit-to-byte-budget pipeline (`utils/fit-image-to-byte-budget.ts`, built on sharp) makes oversized images deliverable: EXIF auto-orient → resize long edge to ≤1568px → walk a fixed quality ladder (JPEG via mozjpeg for opaque images, WebP for alpha — PNG has no quality knob) → shrink dimensions by √(budget/actual) if the ladder floor still exceeds the budget. Deterministic and terminating (bounded attempts, 64px floor); sharp's default `limitInputPixels` stays active as the decompression-bomb guard. The budget (`MAX_IMAGE_OUTPUT_BYTES`, default 48 KiB binary) is sized for the tightest mainstream client cap.
2. **Canvas** (`.canvas`) linearizes to markdown via the pure `obsidian-markdown/canvas.ts` parser ([JSON Canvas 1.0](https://jsoncanvas.org)): group membership by spatial rect containment (innermost group wins; equal rects tiebreak deterministically by id), nodes in reading order (y, then x), and an edge list with node ids resolved to display names. Lenient parsing — unknown properties ignored, malformed entries skipped. `raw: true` skips the linearizer and returns the JSON source verbatim for full structural fidelity.
3. **Text formats** (`.svg`/`.json`/`.txt`/`.csv`/`.xml`/`.log`/`.base`) pass through verbatim as text, capped at a fixed 100 KiB output size (explicit error over silent truncation).
4. **PDFs** return a structured not-yet-supported error carrying the file's existence and size (text extraction is a planned follow-up); unknown types return an error naming the readable set.
4. **PDFs** (`.pdf`) return structured markdown reconstructed from layout-aware extraction (`unpdf`, based on Mozilla's PDF.js). A document metadata header (title, page count, link count), heading hierarchy inferred from relative font sizes, fenced code blocks detected via monospace fonts, page separators, and a deduplicated links footer. Scanned or image-only PDFs with no extractable text return an error stating the file's existence, size, and page count.
5. **Unknown types** return an error naming the readable set.

The extension-to-representation routing above is implemented by the `vault-operations/asset-operations.ts` use-case. Beneath it, every read goes through `vaultFs.readAsset`, which applies the same `resolveSafePath` traversal guard as notes, rejects `.md` paths (notes belong to `vault_read_note`), and enforces a stat-before-read size cap (`MAX_ASSET_BYTES`, default 50 MiB).

Expand Down
3 changes: 3 additions & 0 deletions DOCKERHUB.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Your notes embed screenshots, reference architecture diagrams, and link out to c

- **Images** — the image itself, not the filename. Screenshots and diagrams are downscaled and recompressed server-side when they exceed what MCP clients accept, so even a phone session can look at a 5MB architecture diagram
- **Canvases** — a [Canvas](https://help.obsidian.md/canvas) board arrives as a readable outline: its groups, each card's content in reading order, and the connections between them. The exact JSON source is one flag away when full fidelity matters
- **PDFs** — text is extracted from the document's content streams; scanned or image-only PDFs return an error stating the page count
- **Text and data files** — SVG, JSON, CSV, logs, and [Bases](https://help.obsidian.md/bases) files return exactly as written
- **Browse** — list any folder's assets with per-extension counts and file sizes; assets a note links to report their size in the link graph too

Expand Down Expand Up @@ -152,6 +153,8 @@ All settings are environment variables with sensible defaults.
| `LOG_DIR` | — | `/data/logs` (remote), unset (local) | Directory for persistent log files. When set, logs are written to date-stamped files there alongside stdout. Unset means stdout only. |
| `LOG_RETENTION_DAYS` | — | `30` | Days to keep log files before automatic cleanup on startup |
| `WINDOWS_MODE` | — | `false` | On Windows? Set `true`. Switches the file watcher to polling and note moves to rename-based writes so a vault on a `C:` drive works through Docker Desktop. Safe to leave on for any Windows setup; unneeded on macOS/Linux/WSL2. |
| `MAX_ASSET_BYTES` | — | `52428800` (50 MiB) | Maximum file size `vault_read_asset` will read (in bytes). Files exceeding this are rejected before reading. Raise for vaults with very large individual files. |
| `MAX_IMAGE_OUTPUT_BYTES` | — | `49152` (48 KiB) | Byte budget for images delivered by `vault_read_asset`, in binary bytes before base64 encoding. Images exceeding this are downscaled and recompressed to fit. Sized for the tightest mainstream MCP client cap; raise for clients that accept larger responses. |

## Deployment Options

Expand Down
37 changes: 20 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ Your notes embed screenshots, reference architecture diagrams, and link out to c

- **Images** — the image itself, not the filename. Screenshots and diagrams are downscaled and recompressed server-side when they exceed what MCP clients accept, so even a phone session can look at a 5MB architecture diagram
- **Canvases** — a [Canvas](https://help.obsidian.md/canvas) board arrives as a readable outline: its groups, each card's content in reading order, and the connections between them. The exact JSON source is one flag away when full fidelity matters
- **PDFs** — text is extracted from the document's content streams; scanned or image-only PDFs return an error stating the page count
- **Text and data files** — SVG, JSON, CSV, logs, and [Bases](https://help.obsidian.md/bases) files return exactly as written
- **Browse** — list any folder's assets with per-extension counts and file sizes; assets a note links to report their size in the link graph too

Expand Down Expand Up @@ -295,23 +296,25 @@ These are conventions, not requirements — Vault Cortex works with any property

All settings are environment variables with sensible defaults.

| Variable | Required? | Default | Description |
| --------------------------- | ----------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MCP_AUTH_TOKEN` | Yes | — | Bearer token for authentication (also the JWT signing key) |
| `VAULT_PATH` | Local only | — | Host path to your vault (bind mount source; remote uses a named volume) |
| `PUBLIC_URL` | Remote only | — | Public URL for OAuth discovery metadata |
| `EMBEDDING_ENABLED` | — | `true` | Set `false` to disable the embedding pipeline — skips model download, vector tables, embedding passes, and hybrid search. Search falls back to FTS5 keyword matching. |
| `RERANK_MODE` | — | `blended` | Cross-encoder reranking mode: `blended` applies position-aware score blending after RRF fusion (~200ms added latency), `none` skips reranking. Only takes effect when `EMBEDDING_ENABLED` is true. |
| `MEMORY_ENABLED` | — | `true` | Set `false` to fully disable the memory layer — hides memory tools, skips bootstrap, omits memory from server metadata. `MEMORY_DIR` is ignored when `false`. |
| `MEMORY_DIR` | — | `About Me` | Vault folder for structured memory files |
| `PROTECTED_PATHS` | — | `MEMORY_DIR, Daily Notes` | Folders that `vault_delete_note` refuses to touch |
| `ORPHAN_EXCLUDE_FOLDERS` | — | `Daily Notes, Templates, MEMORY_DIR` | Folders excluded from orphan detection |
| `TZ` | — | `UTC` | IANA timezone for timestamps and daily note resolution |
| `SERVICE_DOCUMENTATION_URL` | — | GitHub repo URL | URL returned in OAuth discovery metadata |
| `LOG_LEVEL` | — | `info` | Logging verbosity: `debug`, `info`, `warn`, `error` |
| `LOG_DIR` | — | `/data/logs` (remote), unset (local) | Directory for persistent log files. When set, logs are written to date-stamped files there alongside stdout. Unset means stdout only. |
| `LOG_RETENTION_DAYS` | — | `30` | Days to keep log files before automatic cleanup on startup |
| `WINDOWS_MODE` | — | `false` | On Windows? Set `true`. Switches the file watcher to polling and note moves to rename-based writes so a vault on a `C:` drive works through Docker Desktop. Safe to leave on for any Windows setup; unneeded on macOS/Linux/WSL2. |
| Variable | Required? | Default | Description |
| --------------------------- | ----------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MCP_AUTH_TOKEN` | Yes | — | Bearer token for authentication (also the JWT signing key) |
| `VAULT_PATH` | Local only | — | Host path to your vault (bind mount source; remote uses a named volume) |
| `PUBLIC_URL` | Remote only | — | Public URL for OAuth discovery metadata |
| `EMBEDDING_ENABLED` | — | `true` | Set `false` to disable the embedding pipeline — skips model download, vector tables, embedding passes, and hybrid search. Search falls back to FTS5 keyword matching. |
| `RERANK_MODE` | — | `blended` | Cross-encoder reranking mode: `blended` applies position-aware score blending after RRF fusion (~200ms added latency), `none` skips reranking. Only takes effect when `EMBEDDING_ENABLED` is true. |
| `MEMORY_ENABLED` | — | `true` | Set `false` to fully disable the memory layer — hides memory tools, skips bootstrap, omits memory from server metadata. `MEMORY_DIR` is ignored when `false`. |
| `MEMORY_DIR` | — | `About Me` | Vault folder for structured memory files |
| `PROTECTED_PATHS` | — | `MEMORY_DIR, Daily Notes` | Folders that `vault_delete_note` refuses to touch |
| `ORPHAN_EXCLUDE_FOLDERS` | — | `Daily Notes, Templates, MEMORY_DIR` | Folders excluded from orphan detection |
| `TZ` | — | `UTC` | IANA timezone for timestamps and daily note resolution |
| `SERVICE_DOCUMENTATION_URL` | — | GitHub repo URL | URL returned in OAuth discovery metadata |
| `LOG_LEVEL` | — | `info` | Logging verbosity: `debug`, `info`, `warn`, `error` |
| `LOG_DIR` | — | `/data/logs` (remote), unset (local) | Directory for persistent log files. When set, logs are written to date-stamped files there alongside stdout. Unset means stdout only. |
| `LOG_RETENTION_DAYS` | — | `30` | Days to keep log files before automatic cleanup on startup |
| `WINDOWS_MODE` | — | `false` | On Windows? Set `true`. Switches the file watcher to polling and note moves to rename-based writes so a vault on a `C:` drive works through Docker Desktop. Safe to leave on for any Windows setup; unneeded on macOS/Linux/WSL2. |
| `MAX_ASSET_BYTES` | — | `52428800` (50 MiB) | Maximum file size `vault_read_asset` will read (in bytes). Files exceeding this are rejected before reading. Raise for vaults with very large individual files. |
| `MAX_IMAGE_OUTPUT_BYTES` | — | `49152` (48 KiB) | Byte budget for images delivered by `vault_read_asset`, in binary bytes before base64 encoding. Images exceeding this are downscaled and recompressed to fit. Sized for the tightest mainstream MCP client cap; raise for clients that accept larger responses. |

**Smart defaults:** Setting `MEMORY_DIR` automatically updates the defaults for `PROTECTED_PATHS` and `ORPHAN_EXCLUDE_FOLDERS`. You only set those explicitly for a fully custom list. When `MEMORY_ENABLED` is `false`, the memory layer is fully disabled — memory tools are hidden and the memory folder is not auto-created.

Expand Down
Loading
Loading