diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json index 5272c524c..2519b62c0 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/default.json @@ -1094,7 +1094,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json index 6b28ce559..29f788b54 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/disabled-tools.json @@ -1036,7 +1036,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json index ee3201a0c..effc9efd8 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/embedding-off.json @@ -1096,7 +1096,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json index f31a4543d..92905468a 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off+embedding-off.json @@ -1012,7 +1012,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json index 2665324fd..d284c4b8c 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/file-tools-off.json @@ -1011,7 +1011,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json index 2add073b7..8b639743f 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+embedding-off.json @@ -957,7 +957,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json index 20dde2b19..ffac4f220 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off+embedding-off.json @@ -873,7 +873,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json index bfce05e7a..f11266c8d 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off+file-tools-off.json @@ -872,7 +872,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json index 0835a604a..6e7ef8f03 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/memory-off.json @@ -956,7 +956,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json index 287872d34..746a63c04 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/obsidian-sync.json @@ -1096,7 +1096,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note, and edit via vault_replace_in_note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json index 021662a9b..9bfacb887 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+embedding-off.json @@ -679,7 +679,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json index 0d9cb764f..6e3a5baf8 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off+embedding-off.json @@ -595,7 +595,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json index f02053ecc..075239886 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+file-tools-off.json @@ -594,7 +594,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json index bf0c4a5a3..bc7f83bdf 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+embedding-off.json @@ -586,7 +586,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json index 1061601c6..e0fed47f2 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off+embedding-off.json @@ -502,7 +502,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json index c643e7c34..d6253d90a 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off+file-tools-off.json @@ -501,7 +501,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json index e09a26b91..c09c73e4e 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly+memory-off.json @@ -585,7 +585,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json index ac82ad67e..dd134ed8c 100644 --- a/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json +++ b/src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/readonly.json @@ -678,7 +678,7 @@ { "name": "vault_read_note", "title": "Read Note", - "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical.\n\nErrors:\n- \"note not found\" — no note exists at this path; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n\nReturns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\").\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", + "description": "Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading.\n\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\" })\nExample: vault_read_note({ path: \"Projects/vault-cortex.md\", properties_only: true })\nExample: vault_read_note({ path: \"TASKS.md\", outline: true })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Active\" })\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", heading_level: 2 }) // disambiguate when several \"Done\" headings exist\nExample: vault_read_note({ path: \"TASKS.md\", heading: \"Done\", start_line: 1, limit: 20 }) // first 20 lines of an oversized section\n\nWhen to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: \"...\" to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total.\nPrefer vault_search when you don't know the path. For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown. Prefer vault_get_memory for About Me/ files (returns content without properties). To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links.\n\nSection boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included.\n\nErrors:\n- \"note not found\" / \"ENAMETOOLONG: …\" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes\n- \"heading not found\" — no heading matches the text; error lists available headings\n- \"ambiguous heading\" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level\n- \"outline, heading, and properties_only are mutually exclusive\" — only one mode per call\n- \"heading_level requires a heading\" — heading_level only disambiguates a heading; pass heading with it\n- \"heading cannot be empty\" — heading is whitespace only; pass the heading's text\n- \"line paging is not available in outline mode\" / \"... properties_only mode\" — start_line/limit only work on text renditions (full read or heading section)\n- \"start line past the end\" — start_line exceeds the rendition's line count; error states the total\n- 'path must end in \".md\"' — add the extension to a note's path; read other files (images, .canvas, data files) with vault_read_file\n- \"absolute path blocked\" / \"path traversal blocked\" / \"hidden path blocked\" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it\n- \"properties block is not valid YAML …\" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it\n- \"EACCES: …\" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it\n\nReturns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block (\"path — lines 1–20 of 250 (continue with start_line: 21)\"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings.\n\nOutline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings (\"##\" with no text) appear with text: \"\" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note.", "inputSchema": { "type": "object", "properties": { diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 589070b7f..1db767a7a 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -2560,7 +2560,7 @@ describe("DISABLED_TOOLS", () => { " Prefer vault_get_memory for About Me/ files (returns content without properties). To edit a section you've read, use vault_patch_note. To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links." expect(readNoteRoutingLine("")).toBe( - `${ROUTING_LINE_START} For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown.${ROUTING_LINE_END}`, + `${ROUTING_LINE_START} For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown.${ROUTING_LINE_END}`, ) expect(readNoteRoutingLine("vault_list_tasks")).toBe(`${ROUTING_LINE_START}${ROUTING_LINE_END}`) }) diff --git a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts index 705fe95e2..0b19f6954 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-surface-snapshot.test.ts @@ -36,7 +36,7 @@ describe("tool surface baseline", () => { * - The allowance is about 2% above the default combo's average when the * cap was set (4,130 chars per tool), so only small growth passes. * Raising the allowance is a deliberate change with its reason in the PR. */ - const CHARS_PER_TOOL_ALLOWANCE = 4226 + const CHARS_PER_TOOL_ALLOWANCE = 4234 it.each(SIZE_CAPPED_COMBO_NAMES)( "combo %s stays within the average per-tool size allowance", diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index 1c4b130ba..5a50195d2 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -71,7 +71,7 @@ export const registerVaultCrudTools = ({ TOOL_NAMES.VAULT_READ_NOTE, { title: "Read Note", - description: `Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. + description: `Read a markdown note by its vault-relative path. By default returns the full raw content including properties; optional modes return just the properties, just the heading outline, or just one section — so large notes don't blow the token budget. Set at most one of properties_only, outline, or heading. Example: vault_read_note({ path: "Projects/vault-cortex.md" }) Example: vault_read_note({ path: "Projects/vault-cortex.md", properties_only: true }) @@ -80,13 +80,13 @@ Example: vault_read_note({ path: "TASKS.md", heading: "Active" }) Example: vault_read_note({ path: "TASKS.md", heading: "Done", heading_level: 2 }) // disambiguate when several "Done" headings exist Example: vault_read_note({ path: "TASKS.md", heading: "Done", start_line: 1, limit: 20 }) // first 20 lines of an oversized section -When to use: You know the exact path and need a specific note's content. For a large note (a long board or doc), use outline: true to see its headings and any text sitting above them, then heading: "..." to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total. -Prefer vault_search when you don't know the path.${whenToolEnabledText("vault_list_tasks", " For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's verbatim Markdown.")}${whenToolEnabledText("vault_get_memory", ` Prefer vault_get_memory for ${config.memoryDir}/ files (returns content without properties).`)}${whenToolEnabledText("vault_patch_note", " To edit a section you've read, use vault_patch_note.")} To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links. +When to use: You know the exact path and need a specific note's content. For a large note (a long Kanban board or doc), use outline: true to see its headings and any text sitting above them, then heading: "..." to read just the one section you need — both far cheaper than pulling the whole file. Use properties_only: true when you only need properties. For an oversized note or section, page it with start_line and limit to read a window at a time. To check a note's or section's line count, request start_line: 1 with limit: 1 — one line plus the total. +Prefer vault_search when you don't know the path.${whenToolEnabledText("vault_list_tasks", " For task status or order on a board, prefer vault_list_tasks; heading mode returns a lane's Markdown.")}${whenToolEnabledText("vault_get_memory", ` Prefer vault_get_memory for ${config.memoryDir}/ files (returns content without properties).`)}${whenToolEnabledText("vault_patch_note", " To edit a section you've read, use vault_patch_note.")} To explore what links to this note or what it links to, use vault_get_backlinks and vault_get_outgoing_links. -Section boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Modes are mutually exclusive — set at most one of properties_only, outline, or heading. Paged reads normalize line endings to LF; unpaged reads stay byte-identical. +Section boundaries: a section spans from its heading to the next heading of the same or higher level (or EOF). Child headings are included. Errors: -- "note not found" — no note exists at this path; verify it with vault_list_notes +- "note not found" / "ENAMETOOLONG: …" — no note exists at this path, or the path (or a name in it) is longer than the filesystem allows; verify it with vault_list_notes - "heading not found" — no heading matches the text; error lists available headings - "ambiguous heading" — multiple headings match; use heading_level to disambiguate, or read the full note (omit heading) when headings share the same level - "outline, heading, and properties_only are mutually exclusive" — only one mode per call @@ -94,11 +94,12 @@ Errors: - "heading cannot be empty" — heading is whitespace only; pass the heading's text - "line paging is not available in outline mode" / "... properties_only mode" — start_line/limit only work on text renditions (full read or heading section) - "start line past the end" — start_line exceeds the rendition's line count; error states the total -- 'path must end in ".md"' — the path names a non-markdown file${whenToolEnabledText("vault_read_file", "; read files (images, .canvas, data files) with vault_read_file instead")} +- 'path must end in ".md"' — add the extension to a note's path${whenToolEnabledText("vault_read_file", "; read other files (images, .canvas, data files) with vault_read_file")} - "absolute path blocked" / "path traversal blocked" / "hidden path blocked" — use a vault-relative path with no hidden (dot-prefixed) file or folder in it - "properties block is not valid YAML …" — properties_only, outline and heading need readable YAML, while a full read returns the note as it is; the error says how to repair it +- "EACCES: …" or another filesystem error code — the note's file can't be read (permissions, a symbolic link loop); ask the vault's owner to fix it -Returns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block ("path — lines 1–20 of 250 (continue with start_line: 21)"). +Returns: Raw markdown string (default); JSON object of properties, {} when the note has none (properties_only); JSON outline object, shaped as the outline parameter describes (outline); raw markdown of the section, heading line included (heading). When start_line or limit is given, the result is preceded by a window-metadata text block ("path — lines 1–20 of 250 (continue with start_line: 21)"). Unpaged full reads return the file's exact bytes; heading sections and paged reads use LF line endings. Outline: bytes at the root is the whole file's on-disk size and modified is its filesystem modification time; each heading's bytes is the exact UTF-8 byte length of the text that heading mode returns for that section. Empty headings ("##" with no text) appear with text: "" — they act as section boundaries but cannot be targeted by the heading parameter; read the parent section (which includes child headings) or the full note${whenToolEnabledText("vault_replace_in_note", ", and edit via vault_replace_in_note")}.`, inputSchema: { diff --git a/src/vault-mcp/obsidian-markdown/__tests__/frontmatter.test.ts b/src/vault-mcp/obsidian-markdown/__tests__/frontmatter.test.ts index ddb8c66dd..38d5c7447 100644 --- a/src/vault-mcp/obsidian-markdown/__tests__/frontmatter.test.ts +++ b/src/vault-mcp/obsidian-markdown/__tests__/frontmatter.test.ts @@ -139,13 +139,19 @@ describe("parseNote", () => { }) }) - it("parses CRLF frontmatter", () => { - // The \r inside the value is gray-matter's pre-existing CRLF - // handling, pinned as-is — the opener gate must not reject the file - expect(parseNote("---\r\ntitle: x\r\n---\r\nbody\r\n")).toEqual({ - data: { title: "x\r" }, - content: "body\r\n", - }) + it.each([ + { label: "a plain value", block: "title: C", data: { title: "C" } }, + { label: "a quoted value", block: 'title: "C"', data: { title: "C" } }, + { label: "a flow list", block: "tags: [a, b]", data: { tags: ["a", "b"] } }, + { label: "a block list", block: "tags:\r\n - a\r\n - b", data: { tags: ["a", "b"] } }, + { label: "an empty value", block: "due:", data: { due: null } }, + { + label: "the second of two properties", + block: "title: C\r\ntype: x", + data: { title: "C", type: "x" }, + }, + ])("reads $label last in a CRLF block as the LF block reads it", ({ block, data }) => { + expect(parseNote(`---\r\n${block}\r\n---\r\nbody\r\n`)).toEqual({ data, content: "body\r\n" }) }) it("strips a BOM before an ordinary frontmatter block", () => { @@ -198,6 +204,11 @@ describe("parseNote", () => { note: '---\ntitle: "abc\n---\nbody\n', message: 'properties block is not valid YAML at line 2, column 12: Missing closing "quote', }, + { + label: "an unbalanced quote on the last line of a CRLF block", + note: '---\r\ntitle: "abc\r\n---\r\nbody\r\n', + message: 'properties block is not valid YAML at line 2, column 12: Missing closing "quote', + }, { label: "an alias with no anchor, which has no position", note: "---\nsummary: *bold*\n---\nbody\n", @@ -399,6 +410,13 @@ describe("parseNoteForRewrite", () => { ])("accepts $label", ({ note, data }) => { expect(parseNoteForRewrite(note)).toEqual({ data, content: "body\n" }) }) + + it("accepts an empty CRLF block as no properties", () => { + expect(parseNoteForRewrite("---\r\n---\r\nbody\r\n")).toEqual({ + data: {}, + content: "body\r\n", + }) + }) }) // ── splitPropertiesBlock ───────────────────────────────────────── @@ -448,9 +466,15 @@ describe("splitPropertiesBlock", () => { body: "---text\ntitle: x\n---\nbody\n", }, { - label: "CRLF line endings", + label: "CRLF line endings, without the carriage return before the closer", note: "---\r\ntitle: x\r\n---\r\nbody\r\n", - blockText: "\r\ntitle: x\r", + blockText: "\r\ntitle: x", + body: "body\r\n", + }, + { + label: "an empty CRLF block", + note: "---\r\n---\r\nbody\r\n", + blockText: "", body: "body\r\n", }, { @@ -617,6 +641,14 @@ describe("stringifyNote", () => { it.each([ { label: "a valid block", body: "---\ntitle: ok\n---\nbody\n" }, { label: "a CRLF block", body: "---\r\ntitle: ok\r\n---\r\nbody\r\n" }, + { + label: "a CRLF block ending in a quoted value", + body: '---\r\ntitle: "ok"\r\n---\r\nbody\r\n', + }, + { + label: "a CRLF block ending in a list of quoted links", + body: '---\r\ntitle: ok\r\nrelated:\r\n - "[[Note]]"\r\n---\r\nbody\r\n', + }, { label: "a Multi Column opener", body: "--- start-multi-column: X\ntext\n" }, { label: "a horizontal rule with no closer", body: "---\nno closer here\n" }, ])("writes an empty-data body that opens with $label verbatim", ({ body }) => { diff --git a/src/vault-mcp/obsidian-markdown/frontmatter.ts b/src/vault-mcp/obsidian-markdown/frontmatter.ts index a3ffa37f8..05bfda9ff 100644 --- a/src/vault-mcp/obsidian-markdown/frontmatter.ts +++ b/src/vault-mcp/obsidian-markdown/frontmatter.ts @@ -75,16 +75,19 @@ const SPLIT_ONLY_OPTIONS = { * line is a bare `---` AND a later line starts with `---` — the rule * Obsidian's metadata cache follows. Otherwise `blockText` is null and the * body is the whole note. A leading BOM is stripped on both paths, as - * gray-matter does. + * gray-matter does. The body keeps its bytes, line endings included. * * `tasks.findBodyStartLine` finds where the body starts by its own, - * stricter rule: a first line and a later line that are exactly `---`. + * stricter rule: a first line and a later line that are exactly `---`. It + * reads lines from `splitIntoLines`, so a CRLF note's `---\r` counts as `---`. */ export const splitPropertiesBlock = ( content: string, ): { - /** The text between the `---` lines. It starts with the opener line's own - * newline, so its line numbers match the note's. */ + /** The text between the `---` lines. It starts with the end of the opener + * line, line break included, so its line numbers match the note's. Its + * lines keep the note's line breaks, `\r\n` included; only the `\r` before + * the closer is dropped, so the last line reads as in an LF note. */ blockText: string | null body: string } => { @@ -92,8 +95,21 @@ export const splitPropertiesBlock = ( if (hasFrontmatterFences) { const file = matter(content, SPLIT_ONLY_OPTIONS) - return { blockText: file.matter, body: file.content } + + // gray-matter closes the block at `\n---`, which splits the last line's + // `\r\n` break in two and leaves its `\r` at the end of the block: + // - earlier lines need nothing, because the YAML parser reads `\r\n` as a + // line break; + // - this `\r` has no `\n` after it, so the parser would read it as part of + // the last value (`title: "C\r"`), or refuse the block when that value + // is quoted. + // A `\r` here is always half of that break, whatever line endings the + // rest of the note uses, so it is dropped in every note. + const blockText = file.matter.endsWith("\r") ? file.matter.slice(0, -1) : file.matter + return { blockText, body: file.content } } + + // gray-matter strips the BOM on the path above; this path does it here const contentWithoutBom = content.startsWith("\uFEFF") ? content.slice(1) : content return { blockText: null, body: contentWithoutBom } } @@ -202,9 +218,11 @@ const convertDocumentToValue = ( * that would stringify back as UTC-Z. */ const readPropertiesBlock = (blockText: string): PropertiesBlockReading => { - // Each call gets its own counter, because a shared one keeps counting - // across parses and reports later errors on the wrong line. The block - // text starts with the opener's own newline, so its lines are the note's. + // - Each call gets its own counter, because a shared one keeps counting + // across parses and reports later errors on the wrong line. + // - The block text from splitPropertiesBlock starts with the end of the + // opener line, line break included, and keeps the note's line breaks, so + // line N of the block is line N of the note. const lineCounter = new LineCounter() // prettyErrors would append its own position and a multi-line excerpt of // the block to each message; the message below states the position itself @@ -356,7 +374,8 @@ export const stringifyNote = (body: string, data: object): string => { * Replaces a note's whole properties block with `properties` and keeps the * body's bytes exactly, adding no newline; only a leading BOM is dropped, * as on every split. The old block is never parsed, so a block the server - * cannot read or keep can still be replaced. `{}` removes the block. + * cannot read or keep can still be replaced. `{}` removes the block. The new + * block always uses LF, so a CRLF note comes back with mixed line endings. */ export const replacePropertiesBlock = (content: string, properties: object): string => { const { body } = splitPropertiesBlock(content) diff --git a/src/vault-mcp/vault-operations/__tests__/memory-store.test.ts b/src/vault-mcp/vault-operations/__tests__/memory-store.test.ts index b275738b4..a896a21d1 100644 --- a/src/vault-mcp/vault-operations/__tests__/memory-store.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/memory-store.test.ts @@ -1992,6 +1992,37 @@ describe("listMemoryFiles", () => { expect(living?.entry_policy).toBe("living") }) + it("reads entry-policy living as the last property of a file with Windows line endings", async () => { + const content = [ + "---", + "title: Living", + "entry-policy: living", + "---", + "", + "# Living", + "", + "## Upcoming (newest first)", + "- **2026-07-11**: a current-state entry", + "", + ].join("\r\n") + await writeFile(join(vault, "About Me/Living.md"), content, "utf8") + + const outlines = await listMemoryFiles({ vaultPath: vault }, logger) + const living = outlines.find((outline) => outline.file === "Living") + + expect(living).toEqual({ + file: "Living", + title: "Living", + bytes: Buffer.byteLength(content, "utf8"), + entry_policy: "living", + leading_callout: null, + headings: [ + { level: 1, text: "Living" }, + { level: 2, text: "Upcoming (newest first)", entry_count: 1 }, + ], + }) + }) + it("treats an unrecognized entry-policy value as append-only", async () => { // Only the explicit "living" opt-in relaxes append-only; a typo must not // silently authorize destructive maintenance. diff --git a/src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts b/src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts index 1077520dd..1b9976eca 100644 --- a/src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts @@ -2276,6 +2276,18 @@ describe("updateProperties", () => { expect(written).toBe("---\ntitle: Original\nstatus: active\n---\n---\nbody after rule\n") }) + it("keeps a CRLF note's last property without a carriage return and its body's bytes", async () => { + await writeFile(join(vault, "crlf.md"), "---\r\ntitle: C\r\ntype: x\r\n---\r\nbody\r\n", "utf8") + await updateProperties( + { vaultPath: vault, path: "crlf.md", properties: { status: "active" } }, + logger, + ) + + // The new block uses LF; the body keeps its CRLF line breaks + const written = await readFile(join(vault, "crlf.md"), "utf8") + expect(written).toBe("---\ntitle: C\ntype: x\nstatus: active\n---\nbody\r\n") + }) + it("merges new keys without changing body", async () => { await writeFile(join(vault, "test.md"), "---\ntitle: Original\n---\nBody content\n", "utf8") await updateProperties( @@ -3239,6 +3251,17 @@ describe("readNoteSection", () => { ) expect(section).toBe("## Plan\nStep one\n") }) + + it("returns a section of a CRLF note with LF line breaks", async () => { + const content = "---\r\ntitle: C\r\n---\r\n## Plan\r\nStep one\r\n\r\n## Later\r\nStep two\r\n" + await writeFile(join(vault, "crlf.md"), content, "utf8") + + const section = await readNoteSection( + { vaultPath: vault, path: "crlf.md", heading: "Plan" }, + logger, + ) + expect(section).toBe("## Plan\nStep one\n") + }) }) // ── Concurrent writes ───────────────────────────────────────── diff --git a/src/vault-mcp/vault-operations/__tests__/vault-patcher.test.ts b/src/vault-mcp/vault-operations/__tests__/vault-patcher.test.ts index 30061e101..18f5ac8ca 100644 --- a/src/vault-mcp/vault-operations/__tests__/vault-patcher.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/vault-patcher.test.ts @@ -4950,6 +4950,196 @@ duplicate beta }) }) +describe("Windows line breaks (CRLF)", () => { + const CRLF_NOTE = "---\r\ntitle: C\r\n---\r\n# H\r\nA\r\n- [ ] task\r\nB\r\n" + const LF_NOTE = "---\ntitle: C\n---\n# H\nA\n- [ ] task\nB\n" + const NOTES = [ + { label: "an LF note", note: LF_NOTE }, + { label: "a CRLF note", note: CRLF_NOTE }, + ] + const CRLF_CONTENT = "X1\r\nX2" + + describe("replaceInNote", () => { + it("matches old_text copied with CRLF line breaks", async () => { + await writeTestNote("crlf.md", CRLF_NOTE) + const result = await replaceInNote( + { + vaultPath: vault, + path: "crlf.md", + oldText: "- [ ] task\r\n", + newText: "- [x] task\n", + }, + logger, + ) + + expect(result).toEqual({ message: "Replaced 1 occurrence in crlf.md", count: 1 }) + expect(await readTestNote("crlf.md")).toBe("---\ntitle: C\n---\n# H\nA\n- [x] task\nB\n") + }) + + it("converts new_text's CRLF line breaks to LF", async () => { + await writeTestNote("lf.md", LF_NOTE) + await replaceInNote( + { vaultPath: vault, path: "lf.md", oldText: "task", newText: "one\r\ntwo" }, + logger, + ) + + expect(await readTestNote("lf.md")).toBe("---\ntitle: C\n---\n# H\nA\n- [ ] one\ntwo\nB\n") + }) + + it("removes every CRLF match as the LF old_text would, keeping each match's wider gap", async () => { + // The wider gap sits below the first match, so measuring the gaps with + // the unconverted old_text, which is longer, would change the output. + await writeTestNote( + "gaps.md", + "---\r\ntitle: Gaps\r\n---\r\nA\r\n\r\nX\r\nY\r\n\r\n\r\n\r\nB\r\n\r\nX\r\nY\r\n\r\nC\r\n", + ) + const result = await replaceInNote( + { + vaultPath: vault, + path: "gaps.md", + oldText: "X\r\nY\r\n", + newText: "", + replaceAllOccurrences: true, + }, + logger, + ) + + expect(result).toEqual({ message: "Replaced 2 occurrences in gaps.md", count: 2 }) + expect(await readTestNote("gaps.md")).toBe("---\ntitle: Gaps\n---\nA\n\n\n\nB\n\nC\n") + }) + + it("quotes old_text as sent, CRLF line breaks included, when it matches nothing", async () => { + await writeTestNote("crlf.md", CRLF_NOTE) + + await expect( + replaceInNote( + { vaultPath: vault, path: "crlf.md", oldText: "missing\r\n", newText: "" }, + logger, + ), + ).rejects.toThrow(new Error('text not found in "crlf.md": "missing\r\n"')) + expect(await readTestNote("crlf.md")).toBe(CRLF_NOTE) + }) + + it("still finds no match for a lone carriage return", async () => { + await writeTestNote("crlf.md", CRLF_NOTE) + + await expect( + replaceInNote({ vaultPath: vault, path: "crlf.md", oldText: "\r", newText: "" }, logger), + ).rejects.toThrow(new Error('text not found in "crlf.md": "\r"')) + expect(await readTestNote("crlf.md")).toBe(CRLF_NOTE) + }) + }) + + describe("content with CRLF line breaks", () => { + it.each(NOTES)("appends it to $label with LF line breaks", async ({ note }) => { + await writeTestNote("note.md", note) + await patchNote( + { vaultPath: vault, path: "note.md", operation: "append", content: CRLF_CONTENT }, + logger, + ) + + expect(await readTestNote("note.md")).toBe( + "---\ntitle: C\n---\n# H\nA\n- [ ] task\nB\n\nX1\nX2\n", + ) + }) + + it.each(NOTES)( + "prepends it under a heading of $label with LF line breaks", + async ({ note }) => { + await writeTestNote("note.md", note) + await patchNote( + { + vaultPath: vault, + path: "note.md", + operation: "prepend", + heading: "H", + content: CRLF_CONTENT, + }, + logger, + ) + + expect(await readTestNote("note.md")).toBe( + "---\ntitle: C\n---\n# H\nX1\nX2\nA\n- [ ] task\nB\n", + ) + }, + ) + + it.each(NOTES)("replaces a span of $label with it, using LF line breaks", async ({ note }) => { + await writeTestNote("note.md", note) + await replaceSpan( + { vaultPath: vault, path: "note.md", startAnchor: "task", content: CRLF_CONTENT }, + logger, + ) + + expect(await readTestNote("note.md")).toBe("---\ntitle: C\n---\n# H\nA\nX1\nX2\nB\n") + }) + + it.each(NOTES)("inserts it into $label with LF line breaks", async ({ note }) => { + await writeTestNote("note.md", note) + await insertAtAnchor( + { + vaultPath: vault, + path: "note.md", + anchor: "task", + position: "after", + content: CRLF_CONTENT, + }, + logger, + ) + + expect(await readTestNote("note.md")).toBe( + "---\ntitle: C\n---\n# H\nA\n- [ ] task\nX1\nX2\nB\n", + ) + }) + + it("counts the blank CRLF lines at the edges of replacement content as empty lines", async () => { + await writeTestNote("gaps.md", "---\ntitle: Gaps\n---\n\nA\n\nX\n\nB\n") + await replaceSpan( + { vaultPath: vault, path: "gaps.md", startAnchor: "X", content: "\r\n\r\nY\r\n\r\n" }, + logger, + ) + + expect(await readTestNote("gaps.md")).toBe("---\ntitle: Gaps\n---\n\nA\n\n\nY\n\n\nB\n") + }) + + it("still refuses content that repeats the target heading with a CRLF line break", async () => { + await writeTestNote("note.md", "---\r\ntitle: C\r\n---\r\n## H\r\nA\r\n") + + await expect( + patchNote( + { + vaultPath: vault, + path: "note.md", + operation: "append", + heading: "H", + content: "## H\r\nX", + }, + logger, + ), + ).rejects.toThrow( + new Error( + 'content begins with the heading "## H", which would duplicate it — heading-targeted ops keep the matched heading, so omit the heading line from content.', + ), + ) + }) + }) + + it("edits a CRLF note whose last property is a list of quoted links, keeping its properties", async () => { + await writeTestNote( + "related.md", + '---\r\ntitle: "C"\r\nrelated:\r\n - "[[Note]]"\r\n---\r\nA\r\n', + ) + await patchNote( + { vaultPath: vault, path: "related.md", operation: "append", content: "Z" }, + logger, + ) + + expect(await readTestNote("related.md")).toBe( + '---\ntitle: C\nrelated:\n - "[[Note]]"\n---\nA\n\nZ\n', + ) + }) +}) + describe("hidden paths", () => { // The note exists on disk so a removed guard would make the operations // succeed — the tests then fail because the edit went through, never via diff --git a/src/vault-mcp/vault-operations/vault-patcher.ts b/src/vault-mcp/vault-operations/vault-patcher.ts index d14b9ed3a..7dcd4beb1 100644 --- a/src/vault-mcp/vault-operations/vault-patcher.ts +++ b/src/vault-mcp/vault-operations/vault-patcher.ts @@ -55,21 +55,14 @@ type PatchNoteResult = Readonly<{ // ── Internal helpers ──────────────────────────────────────────── /** The heading a content block begins with — the heading on its first non-blank - * line — or null when it starts with anything else. parseHeadings makes this - * fence-aware, so a `## foo` inside an opening code fence is not a heading. - * - * Strips a trailing CR before parsing: HEADING_REGEX uses `(.*)` where `.` - * excludes CR, so a CRLF-authored `## New\r` would otherwise read as ordinary - * text and report nothing. Detection only — callers insert the caller's own - * lines verbatim, line endings untouched. */ + * line (blank: empty or whitespace only) — or null when that line is not a + * heading or every line is blank. parseHeadings makes this fence-aware, so a + * `## foo` inside an opening code fence is not a heading. */ const leadingHeadingOfContent = (contentLines: readonly string[]): HeadingInfo | null => { - const normalizedLines = contentLines.map((line) => { - return line.endsWith("\r") ? line.slice(0, -1) : line - }) - const firstContentLineIndex = normalizedLines.findIndex((line) => line.trim() !== "") + const firstNonBlankLineIndex = contentLines.findIndex((line) => line.trim() !== "") return ( - parseHeadings(normalizedLines).find( - (contentHeading) => contentHeading.startLine === firstContentLineIndex, + parseHeadings(contentLines).find( + (contentHeading) => contentHeading.startLine === firstNonBlankLineIndex, ) ?? null ) } @@ -232,6 +225,8 @@ type LineBreakRun = Readonly<{ start: number; end: number }> /** Edit points for a `replaceInNote` deletion. `bodyParts` is the body split at * every removed match, so `bodyParts.join(removedText)` is the original body. + * - Every line break is a single `\n`: the body comes from splitIntoLines, and + * replaceInNote converts the caller's `\r\n` before matching. * - Only a match whose removal leaves an empty line where it sat gets an edit * point; any other removal is written exactly as asked. * - Each gap counts the empty lines between the match's text and the nearest @@ -278,6 +273,8 @@ const listTextDeletionEdits = (params: { updatedBodyOffset += partBeforeMatch.length updatedLineIndex += partBeforeMatch.split("\n").length - 1 + // The removal leaves an empty line when a line break or the body's edge + // sits on both sides of the point where the match was. const startsLine = updatedBodyOffset === 0 || updatedBody[updatedBodyOffset - 1] === "\n" const endsLine = updatedBodyOffset === updatedBody.length || updatedBody[updatedBodyOffset] === "\n" @@ -329,6 +326,8 @@ const resolveAnchorLine = (params: { }): number => { const { lines, anchor, fromLine, firstMatch, path, role } = params const anchorLabel = role ? `${role} anchor` : "anchor" + // Anchors get no line-break conversion, unlike old_text: an anchor is part + // of one line, so a line break in it would match no line either way. const matchingLineIndices = lines.flatMap((line, index) => { return index >= fromLine && line.includes(anchor) ? [index] : [] }) @@ -405,7 +404,9 @@ const patchNote = async ( const lockPath = resolveSafePath(params.vaultPath, path) return withExclusiveFileLock(lockPath, async () => { const { fullPath, data, lines, beforeBytes } = await readNoteForPatch(params.vaultPath, path) - const contentLines = content.split("\n") + // The control-character check allows `\r`, so content may hold `\r\n` + // breaks; splitIntoLines turns them into the LF lines the body uses. + const contentLines = splitIntoLines(content) // File-level operation (no heading target) if (!heading) { @@ -467,6 +468,8 @@ const patchNote = async ( // Replace on a section with child headings requires explicit opt-in — // without it, the caller may not realize children will be destroyed. if (operation === "replace" && !includeChildren) { + // A section's body runs to the next heading of the same or higher level, + // so every heading inside that range is a child of the target. const childHeadings = headings.filter((candidate) => { return ( candidate.startLine >= target.bodyStartLine && candidate.startLine < target.bodyEndLine @@ -512,36 +515,46 @@ const replaceInNote = async ( ): Promise<{ message: string; count: number }> => { const { path, oldText, newText, replaceAllOccurrences } = params - if (oldText.length === 0) { + if (oldText === "") { throw new Error("oldText cannot be empty") } + // Only new_text is written to the note; old_text is only matched against it assertNoControlCharacters(newText, "new_text") + // The body is matched with LF line breaks (readNoteForPatch splits it with + // splitIntoLines), so the caller's `\r\n` breaks become `\n` too: + // - text copied from a raw read of a CRLF note then matches; + // - replaceAll never empties a non-empty string, unlike splitIntoLines, which + // would turn old_text "\r" into "", a search text that matches at offset 0. + const searchText = oldText.replaceAll("\r\n", "\n") + const replacementText = newText.replaceAll("\r\n", "\n") + const lockPath = resolveSafePath(params.vaultPath, path) return withExclusiveFileLock(lockPath, async () => { const { fullPath, data, lines, beforeBytes } = await readNoteForPatch(params.vaultPath, path) const body = lines.join("\n") - const firstMatchOffset = body.indexOf(oldText) + const firstMatchOffset = body.indexOf(searchText) + // The message quotes the text the caller sent, before line-break conversion if (firstMatchOffset === -1) { throw new Error(`text not found in "${path}": "${truncateForMessage(oldText)}"`) } const bodyParts = replaceAllOccurrences - ? body.split(oldText) - : [body.slice(0, firstMatchOffset), body.slice(firstMatchOffset + oldText.length)] + ? body.split(searchText) + : [body.slice(0, firstMatchOffset), body.slice(firstMatchOffset + searchText.length)] const count = bodyParts.length - 1 - const updatedLines = bodyParts.join(newText).split("\n") + const updatedLines = bodyParts.join(replacementText).split("\n") // A deletion stacks the empty lines from both sides of each match into one // run, so that run is shrunk back. A replacement keeps new text between - // them, and its line breaks are the caller's, written exactly as given. - const isDeletion = newText.length === 0 + // them, and its line breaks are written where the caller put them. + const isDeletion = replacementText === "" const writtenLines = isDeletion ? collapseEmptyLineRunsAtEdits({ lines: updatedLines, - edits: listTextDeletionEdits({ body, bodyParts, removedText: oldText }), + edits: listTextDeletionEdits({ body, bodyParts, removedText: searchText }), }) : updatedLines @@ -574,7 +587,7 @@ const deleteSpan = async ( ): Promise => { const { path, startAnchor, endAnchor, firstMatch } = params - if (startAnchor.length === 0) { + if (startAnchor === "") { throw new Error("startAnchor cannot be empty") } if (endAnchor === "") { @@ -640,13 +653,13 @@ const replaceSpan = async ( ): Promise => { const { path, startAnchor, endAnchor, content, firstMatch } = params - if (startAnchor.length === 0) { + if (startAnchor === "") { throw new Error("startAnchor cannot be empty") } if (endAnchor === "") { throw new Error("endAnchor cannot be empty") } - if (content.length === 0) { + if (content === "") { throw new Error("content cannot be empty") } assertNoControlCharacters(content, "content") @@ -664,13 +677,15 @@ const replaceSpan = async ( }) const replacedLineCount = endLine - startLine + 1 - const contentLines = content.split("\n") + // splitIntoLines turns content's `\r\n` breaks into the LF lines the body uses + const contentLines = splitIntoLines(content) const updatedLines = lines.toSpliced(startLine, replacedLineCount, ...contentLines) // Content is a side of both edits: its leading empty lines meet the note's // empty lines above the old span, and its trailing ones meet those below. // Each joined run keeps the wider side's count, and the content's interior - // is written as given. + // is written as given. Both boundaries index updatedLines before any run + // is collapsed. const writtenLines = collapseEmptyLineRunsAtEdits({ lines: updatedLines, edits: [ @@ -719,10 +734,10 @@ const insertAtAnchor = async ( ): Promise => { const { path, anchor, position, content, firstMatch } = params - if (anchor.length === 0) { + if (anchor === "") { throw new Error("anchor cannot be empty") } - if (content.length === 0) { + if (content === "") { throw new Error("content cannot be empty") } assertNoControlCharacters(content, "content") @@ -739,10 +754,14 @@ const insertAtAnchor = async ( path, }) - const contentLines = content.split("\n") + // splitIntoLines turns content's `\r\n` breaks into the LF lines the body uses + const contentLines = splitIntoLines(content) const insertIndex = position === "before" ? anchorLine : anchorLine + 1 const updatedLines = lines.toSpliced(insertIndex, 0, ...contentLines) + // No empty-line collapse, unlike the span edits: no text is removed, so no + // two runs of the note's empty lines end up touching, and the content's + // own empty lines are written as given. const afterBytes = await writePatchedNote(fullPath, data, updatedLines, logger) logger.info("inserted at anchor", {