Per CommonMark: headings may be indented up to 3 spaces, may have no text
(bare #), and may end in an optional closing run of #s (preceded by a
space, followed only by trailing spaces) that isn't part of the heading
text. getNoteOutline missed all three.
Adds 5 regression tests. dist rebuilt.
Addresses the remaining fence state-machine bugs from review:
- track the opening fence's character and marker count, only close on a
run of the same character with length >= the opener's (a shorter run
no longer falsely closes a longer one)
- a closer may only be followed by whitespace; a fence-looking line with
trailing text (e.g. a language tag) is no longer misread as a closer
- allow fence markers indented up to 3 leading spaces, per CommonMark
Adds 6 regression tests covering each case plus mismatched-character and
longer-closer scenarios. dist rebuilt.
- getNoteOutline and readNoteLines now normalizePath() before the
pathFilter.isAllowed() check, matching every other method - previously
both skipped it, so read_note_lines could read files under .obsidian/
that every other tool is blocked from touching.
- getNoteOutline now tracks fenced code block state (``` and ~~~) and
skips lines inside one, so a code sample containing a shell comment or
markdown example no longer gets misdetected as a real heading.
- getNoteOutline now skips the frontmatter block (both LF and CRLF line
endings) before scanning for headings, so a YAML comment line doesn't
get misdetected as an H1.
- readNoteLines clamps both startLine and endLine into [1, lines.length]
instead of passing raw values to Array.slice, which silently wrapped
to the end of the file for startLine <= 0 via slice's negative-index
behavior.
Adds 6 new tests covering fences, LF/CRLF frontmatter, and the three
clamping edge cases. Full suite: 257 passed.
StdioServerTransport only listens for stdin 'data'/'error'; it never
wires up 'end'/'close', so the process never learns when the client
disconnects. Hosts with no MCP lifecycle controls (e.g. Codex 0.146.x)
left orphaned processes accumulating with nothing to reap them.
Exit gracefully via server.close() on stdin EOF and on SIGTERM/SIGINT.
Obsidian emits path-qualified links when a basename is ambiguous.
findPathForWikiLink matched basenames only, so these always returned
no match. Names containing '/' now match the full vault-relative path.
String.replace interprets $$, $&, $` and $' in the replacement even
for plain-string searches, corrupting notes whose newString contains
them. Use a replacer function so newString is inserted verbatim.
Fixes#149
search_notes now accepts optional pathPrefix (restrict to a subtree) and
excludePaths (skip subtrees). Filtering happens before file I/O, so excluded
files are never read or scored, and the result limit is no longer diluted by
results from unwanted directories. Directory-prefix matching is slash-tolerant
and does not over-match sibling names (Projects vs Project-notes). +5 tests.
PathFilter only denied .git/.obsidian/node_modules at the vault root because
the glob patterns are anchored. Nested copies (tools/cli/node_modules/...,
tools/repo/.git/config, nested .obsidian) were traversable: nested
node_modules polluted listAllTags with garbage tags, and nested .git/.obsidian
exposed their contents (incl. .git/config credentials) under the same
prompt-injection threat model as the case-equivalence bypass.
Deny these restricted names as ANY path segment (case-insensitive), in
addition to the existing glob patterns. Legit paths that merely contain the
substring (gitignore-tips.md, my-node_modules-notes.md) stay allowed. +5 tests.
Bumps to 0.11.5.
writeNote mislabeled any error whose message merely contained 'space' as
'No space left on device' (and 'permission'/'access' as permission denied),
producing false ENOSPC reports when the disk had ample space. Replace the
substring matching with a code-based classifier (ENOSPC/EACCES/EPERM/EROFS),
preserve our own clear errors as-is, and keep the original message in the
generic fallback. Adds unit tests for the classifier.
Does not address the secondary same-path write race noted in #109 (no
deterministic repro); the misclassification was the actual false report.
PathFilter compiled deny-list patterns case-sensitively and matched the
path verbatim, so on case-insensitive filesystems (macOS, Windows) names
like .Git/config, .GIT/config, or .oBsIdIaN/secrets.md slipped past the
restriction while the OS opened the real .git/.obsidian file. Windows
trailing dots/spaces (.git./config, .git /config) bypassed it the same way.
Match case-insensitively and canonicalize each path segment (strip trailing
dots/spaces, drop ./ and empty segments) before checking the deny-list.
Covers isAllowed and isAllowedForListing. Adds the full bypass matrix as
tests. (CWE-178 / CWE-41)
* fix: guard undefined/non-string path in PathFilter (closes#107)
read_note/write_note (and patch/delete/move) threw an unguarded TypeError
'Cannot read properties of undefined (reading 'replace')' when invoked
without a path. Add a typeof guard in PathFilter.isAllowed and
isAllowedForListing that returns a clear 'path is required and must be a
string' error instead. Covers all callers in one place. Adds 3 tests.
* chore: bump version to 0.11.3
Ships the #107 PathFilter undefined-path guard. Patch release.
- none (default): permanent delete (previous behavior)
- local: move to .trash/ inside vault, preserving folder structure
- system: move to OS trash via the trash package
- Add trashMode parameter to tool schema, types, and handler
- Add tests for local and system trash modes
- Update docs: CHANGELOG, README, website
- Change validation from falsy check to explicit undefined/null check
- Empty string is now a valid way to delete matched text
- Update test to verify deletion behavior
The pathfilter performance test was failing on CI runners due to
variability in execution speed. Increased threshold from 100ms to 200ms
for 8000 checks to account for slower CI environments while still
catching real performance regressions.
Fixes flaky test: "pathfilter performance with many checks"
Implements clickable URI links using Obsidian's URI protocol to enable
direct navigation to notes from AI assistant responses.
- Add generateObsidianUri() utility function with proper URI encoding
- Include obsidianUri field in SearchResult, BatchReadResult, and NoteInfo
- Use absolute path format (obsidian:///path/to/note) for compatibility
- Add comprehensive test coverage for URI generation
Resolves#5
- Implement --version/-v flag to display version number
- Implement --help/-h flag to show usage information
- Version now dynamically read from package.json
- Update error message to suggest --help for more info
Introduces a new patch_note tool that allows replacing specific strings in notes without rewriting the entire file, reducing token usage and improving performance for small edits.
Features:
- String-based replacement with oldString/newString parameters
- Safety: fails if multiple matches found (unless replaceAll=true)
- Preserves frontmatter and file structure
- Returns match count and detailed success/error messages
Changes:
- Added PatchNoteParams and PatchNoteResult types
- Implemented patchNote() method in FileSystemService
- Added patch_note tool to MCP server
- Comprehensive test coverage (8 new tests)
- Bumped version to 0.6.0
- Remove js-yaml dependency and use gray-matter for all YAML operations
- Fix write_note, update_frontmatter, and manage_tags YAML serialization errors
- Add comprehensive integration tests for all frontmatter operations
- Tests cover write_note with frontmatter, append/prepend modes, update operations
- Tests cover tag management (add, remove, list) and edge cases
- All 22 tests now passing including frontmatter validation
- Version bump to 0.5.4
Resolves: 'yaml.dump is not a function' error in all frontmatter tools
- Normalize '.' path to empty string for root directory listing
- Ensure consistent behavior between '.', '/', and '' paths
- All now correctly return both files and directories from vault root
- Version bump to 0.5.3
Resolves issue where LLMs using '.' path only received files without directories.
- Change main field from server.ts to dist/server.js
- Add executable permission to compiled JavaScript file
- This resolves the 'env: bun: No such file or directory' error
- Version bump to 0.5.2