Skip to content

feat(vault_read_note): add outline and section-scoped reads#130

Merged
aliasunder merged 11 commits into
mainfrom
claude/vault-bootstrap-setup-numpkm
Jun 15, 2026
Merged

feat(vault_read_note): add outline and section-scoped reads#130
aliasunder merged 11 commits into
mainfrom
claude/vault-bootstrap-setup-numpkm

Conversation

@aliasunder

@aliasunder aliasunder commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Why

vault_read_note only ever returned the entire note body. For large notes (a long Kanban board, a long CLAUDE.local.md) that exceeds the tool-result token budget and truncates. The write side already targets sections by heading (vault_patch_note), so an agent could surgically edit ## Active without reading ## Done — yet couldn't read ## Active without pulling the whole file. This closes that read/write asymmetry.

What

Two new optional, mutually-exclusive params on vault_read_note:

  • outline: true — returns the heading tree as JSON ({ level, text, bytes } per heading), no bodies. A cheap structure fetch: read the outline, then drill into the one section you need.
  • heading (+ optional heading_level) — returns just that section: the heading line plus its body, through the next heading of the same or higher level (child headings included). Case-sensitive exact match, mirroring vault_patch_note's targeting.

properties_only is unchanged; default (no mode) still returns the full note. The handler rejects combining more than one mode.

How

The section-span parser (parseHeadings / findHeading) is extracted from vault-patcher.ts into a new shared heading-parser.ts, so read and patch resolve a section to the exact same span — "read section X" and "edit section X" line up, and there's no second parser to drift. The move is behaviour-preserving; the patcher's existing tests pass unchanged.

Notes on semantics

  • A parent heading's span (and its outline bytes) includes its nested child sections — consistent with what a section read of that heading returns.
  • The trailing Obsidian %% settings block (e.g. a Kanban board's settings) is excluded from the last section, reusing the patcher's existing boundary logic.
  • Section line ranges are body-relative (frontmatter excluded), matching the patcher.

Tests

  • New heading-parser.test.ts — direct unit tests for the extracted parser (H1–H6 spans, child inclusion, fenced-code headings ignored, trailing %% exclusion, not-found/ambiguous/empty errors).
  • vault-filesystem.test.tsreadNoteOutline (level/text/bytes, no-headings → [], frontmatter excluded, not-found) and readNoteSection (heading + body, child inclusion, heading_level disambiguation, trailing %% exclusion, not-found/ambiguous).
  • tool-definitions.test.ts — handler rejects combining modes.

657 tests pass; lint, build, and prettier:check clean. AGENTS.md structure map updated to list heading-parser.ts; tool count unchanged (params added, no new tool).


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added outline mode to vault_read_note tool, returning a heading structure as JSON.
    • Added heading parameter to vault_read_note for extracting specific sections by heading text.
    • Added optional heading_level parameter for disambiguating duplicate headings.
  • Tests

    • Added comprehensive test coverage for heading parsing and section extraction.
  • Refactor

    • Extracted shared heading parsing logic into a dedicated module.

vault_read_note returned the whole note body, so large notes (a long
Kanban board, CLAUDE.local.md) blow the tool-result token budget and
truncate. The write side already targets sections by heading
(vault_patch_note); this closes the read/write asymmetry.

Two new optional, mutually-exclusive modes:
- outline: true — returns the heading tree as JSON ({ level, text, bytes }
  per heading), a cheap structure fetch.
- heading (+ heading_level) — returns just that section (heading line +
  body, child headings included), mirroring vault_patch_note's targeting.

Extracts the section-span parser (parseHeadings/findHeading) from
vault-patcher.ts into a shared heading-parser.ts so read and patch resolve
sections identically — no second parser. Pure move; patcher behavior and
tests unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@aliasunder, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 17 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eeeb7bbd-21ff-434c-8877-db8f1bd61dfb

📥 Commits

Reviewing files that changed from the base of the PR and between 5b8c1af and 613726f.

📒 Files selected for processing (5)
  • .coderabbit.yaml
  • src/vault-mcp/__tests__/tool-definitions.test.ts
  • src/vault-mcp/tool-definitions.ts
  • src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts
  • src/vault-mcp/vault-operations/vault-filesystem.ts
📝 Walkthrough

Walkthrough

A new shared heading-parser.ts module is extracted from vault-patcher.ts, providing parseHeadings, findTrailingCommentBlockStart, and findHeading. vault-filesystem.ts gains readNoteOutline and readNoteSection using these helpers. The VAULT_READ_NOTE tool handler is extended with mutually-exclusive outline, heading, and heading_level input modes routed to the new filesystem functions.

Changes

Heading Parser Extraction and Note Read Modes

Layer / File(s) Summary
Shared heading-parser module
src/vault-mcp/vault-operations/heading-parser.ts, src/vault-mcp/vault-operations/__tests__/heading-parser.test.ts, AGENTS.md
New module exports HeadingInfo, parseHeadings, findTrailingCommentBlockStart, and findHeading with fenced-code and Obsidian %% comment block awareness. Full Vitest coverage for span calculations, nesting, disambiguation, and error cases. AGENTS.md updated to document the new module.
vault-patcher delegates to heading-parser
src/vault-mcp/vault-operations/vault-patcher.ts
~245 lines of inline heading parsing and trailing-comment-block detection removed; replaced with imports from ./heading-parser.js. vaultPatcher API surface is unchanged.
readNoteOutline and readNoteSection in vault-filesystem
src/vault-mcp/vault-operations/vault-filesystem.ts, src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts
HeadingOutline type and readNoteOutline/readNoteSection functions added, with frontmatter exclusion, per-section byte sizing, and Kanban block stripping. vaultFs export updated. Tests cover outline structure, disambiguation, error messages, and missing-note behavior.
VAULT_READ_NOTE new read modes
src/vault-mcp/tool-definitions.ts, src/vault-mcp/__tests__/tool-definitions.test.ts
Tool description, Zod schema (outline, heading, heading_level), and handler updated with upfront mutual-exclusivity validation and routing to readNoteOutline or readNoteSection. Error-handling test added for the outline+heading conflict case.

Sequence Diagram

sequenceDiagram
  participant Client as MCP Client
  participant Handler as VAULT_READ_NOTE Handler
  participant vaultFs as vault-filesystem
  participant headingParser as heading-parser

  Client->>Handler: { path, outline: true }
  Handler->>vaultFs: readNoteOutline(path)
  vaultFs->>headingParser: parseHeadings(lines)
  headingParser-->>vaultFs: HeadingInfo[]
  vaultFs-->>Handler: HeadingOutline[]
  Handler-->>Client: outline JSON

  Client->>Handler: { path, heading: "Section A", heading_level: 2 }
  Handler->>vaultFs: readNoteSection(path, "Section A", 2)
  vaultFs->>headingParser: parseHeadings(lines)
  headingParser-->>vaultFs: HeadingInfo[]
  vaultFs->>headingParser: findHeading(headings, "Section A", 2)
  headingParser-->>vaultFs: HeadingInfo
  vaultFs-->>Handler: section markdown text
  Handler-->>Client: section markdown
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 A parser was hidden, deep in the patch,
Now shared by all readers — a cleaner dispatch!
Outlines and sections, each heading in view,
The Kanban block dodged, and the fence-states held true.
One module to rule them, extracted with care —
Hop hop, little parser, now everyone shares! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding outline and section-scoped read modes to vault_read_note, which is the central feature described in all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/vault-bootstrap-setup-numpkm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vault-mcp/tool-definitions.ts`:
- Around line 146-160: The tool schemas allow heading_level to be specified
independently and accept empty string values for heading, both of which bypass
section mode validation and fall back to full-note reads. For each tool
definition in src/vault-mcp/tool-definitions.ts at lines 146-160 (anchor),
181-189, and 207-217 (siblings), add schema validation to ensure heading_level
can only be used when heading is present and non-empty, and to reject empty
string values for heading. Use Zod's refine or superRefine method on the
parameter object to add a constraint that validates heading_level is only set
when heading is a non-empty string, so section mode is properly enforced before
dispatch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 20c2778f-298a-430c-96f3-aaa487eea2df

📥 Commits

Reviewing files that changed from the base of the PR and between 598c43d and 5b8c1af.

📒 Files selected for processing (8)
  • AGENTS.md
  • src/vault-mcp/__tests__/tool-definitions.test.ts
  • src/vault-mcp/tool-definitions.ts
  • src/vault-mcp/vault-operations/__tests__/heading-parser.test.ts
  • src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts
  • src/vault-mcp/vault-operations/heading-parser.ts
  • src/vault-mcp/vault-operations/vault-filesystem.ts
  • src/vault-mcp/vault-operations/vault-patcher.ts

Comment thread src/vault-mcp/tool-definitions.ts
CodeRabbit flagged two silent fall-throughs in the new section-read
modes: heading_level passed without heading was ignored, and heading: ""
was treated as falsy and fell back to a full-note read — both contrary
to the fail-loud contract the tool documents.

- Mutual-exclusivity check now treats heading !== undefined as section
  mode (so an empty heading is caught, not skipped).
- Reject heading_level supplied without heading ("heading_level requires
  a heading").
- Dispatch on heading !== undefined so an empty heading reaches
  findHeading and surfaces its "heading cannot be empty" error.

Kept the validation in the handler rather than tightening the Zod schema
(.min(1)/.trim()) so vault_read_note's heading schema stays identical to
vault_patch_note's — schema parity is the point of this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Comment thread src/vault-mcp/tool-definitions.ts Outdated
Prefer vault_search when you don't know the path. Prefer vault_get_memory for ${config.memoryDir}/ files (returns content without properties). To edit a section you've read, use vault_patch_note (same heading targeting).

Returns: Raw markdown string (default), or JSON object of properties (when properties_only: true).`,
Section boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included in the parent section. This matches vault_patch_note exactly, so "read section X" and "edit section X" resolve to the same span.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to specify it matches patch note exactly. This is a tool description and unnecessary tokens.

…tion

The tool description ships to the model on every call, so the
"matches vault_patch_note exactly" sentence and the "(same heading
targeting)" / "Mirrors vault_patch_note's heading targeting" asides were
unnecessary tokens that don't help a caller use the tool. Removed all
three; the behavioral guidance (section boundaries, when-to-use pointer
to vault_patch_note for editing) is kept.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Comment thread src/vault-mcp/tool-definitions.ts
Comment thread src/vault-mcp/tool-definitions.ts
claude added 2 commits June 15, 2026 00:22
Add .min(1) to the heading input schema so an empty heading is rejected
at parse time rather than only at the handler. The handler guards
(heading_level-requires-heading, and findHeading's "heading cannot be
empty" for whitespace-only input) remain as defense-in-depth.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Restore heading-schema parity with vault_read_note: vault_patch_note's
heading was missing .min(1) (omitting it stays valid for file-level
append/prepend, since it's .optional() — only an empty string is now
rejected at parse time).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Comment thread src/vault-mcp/tool-definitions.ts Outdated
if (
[
properties_only === true,
outline === true,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this a clearly named car vs inline

Pull the inline `[...].filter(Boolean).length` mutual-exclusivity check
out into a `selectedModeCount` const so the guard reads clearly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
text: heading.text,
// Section span = heading line through bodyEndLine (the same span a section
// read returns), so the size hint matches what reading it would cost.
bytes: Buffer.byteLength(

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not put this in a var and log it?

Pull the per-section byte computation onto a named `sectionText` var and
log the summed `totalBytes` alongside the heading count, so the outline
read's payload size is observable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Comment thread src/vault-mcp/tool-definitions.ts Outdated
)
}

// An empty heading reaches readNoteSection and surfaces findHeading's

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have a min value check now?

claude added 4 commits June 15, 2026 00:39
The heading schema now enforces min(1), so an empty heading is rejected
before the handler. Reword the section-routing comment to reflect that
the !== undefined check just selects section vs full read.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
heading is either undefined or a non-empty string (schema min(1)), so a
plain truthy `if (heading)` is equivalent to `!== undefined` and reads
cleaner.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Mirror the obsidian-headless-sync-docker fork's config: disable the poem,
sequence diagrams, and review-status sections so the recurring walkthrough
comment (and its webhook payload) stays lean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
Every read mode had an example except heading_level. Add one showing
heading + heading_level together for the disambiguation case.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants