neuro-vault-mcp 10.0.2 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -142,7 +142,7 @@ Two vaults cannot share the same directory basename — the basename doubles as
142
142
  With multiple vaults registered:
143
143
 
144
144
  - **Every tool** accepts an optional `vault: "<name>"` parameter to target a specific vault.
145
- - **`search_notes`, `query_notes`, `get_vault_overview`, `list_tags`, and `list_properties`** fan out across all registered vaults when `vault` is omitted. The response shape switches to `results_by_vault: [...]` (one entry per vault) plus `skipped_vaults: [...]` for any vault the tool could not reach and `failed_vaults: [...]` for per-vault runtime errors (`{ vault, error: { code, message, details? } }`). A single failed vault does not abort the whole call.
145
+ - **`search_notes`, `query_notes`, `get_vault_overview`, and `list_tags`** fan out across all registered vaults when `vault` is omitted. The response shape switches to `results_by_vault: [...]` (one entry per vault) plus `skipped_vaults: [...]` for any vault the tool could not reach and `failed_vaults: [...]` for per-vault runtime errors (`{ vault, error: { code, message, details? } }`). A single failed vault does not abort the whole call.
146
146
  - **All other tools** (writes, reads of specific paths, single-vault diagnostics) require an explicit `vault` in multi-vault mode. Omitting it returns `VAULT_REQUIRED`.
147
147
  - **Semantic fan-out** silently skips vaults whose Smart Connections `.smart-env/multi/` index is unavailable. Targeting such a vault explicitly with `vault: "<name>"` returns `SEMANTIC_INDEX_NOT_FOUND`.
148
148
 
@@ -163,7 +163,7 @@ For other clients (Cursor / Windsurf / npx), see [docs/guide/installation.md](./
163
163
  User guide lives in [`docs/guide/`](./docs/guide/README.md):
164
164
 
165
165
  - [Installation](./docs/guide/installation.md)
166
- - [Semantic Search](./docs/guide/semantic-search.md) — `search_notes`, `get_similar_notes`, `find_duplicates`, `get_stats`
166
+ - [Semantic Search](./docs/guide/semantic-search.md) — `search_notes`, `get_similar_notes`, `find_duplicates`
167
167
  - [Vault Operations](./docs/guide/vault-operations.md) — note CRUD, daily notes, properties, tags, structured queries (`query_notes`), vault snapshot (`get_vault_overview`)
168
168
  - [Routing Between Tools](./docs/guide/routing.md)
169
169
  - [Configuration](./docs/guide/configuration.md) — CLI args, troubleshooting, limitations, development
package/dist/cli.d.ts CHANGED
@@ -161,13 +161,6 @@ interface SetPropertyInput {
161
161
  value: PropertyValue;
162
162
  type?: PropertyType;
163
163
  }
164
- interface ReadPropertyInput {
165
- identifier: NoteIdentifier;
166
- name: string;
167
- }
168
- interface ReadPropertyResult {
169
- value: PropertyValue;
170
- }
171
164
  interface RemovePropertyInput {
172
165
  identifier: NoteIdentifier;
173
166
  name: string;
@@ -184,7 +177,6 @@ interface VaultProvider {
184
177
  createNote(input: CreateNoteInput): Promise<CreateNoteResult>;
185
178
  readDaily(): Promise<DailyNoteResult>;
186
179
  setProperty(input: SetPropertyInput): Promise<void>;
187
- readProperty(input: ReadPropertyInput): Promise<ReadPropertyResult>;
188
180
  removeProperty(input: RemovePropertyInput): Promise<void>;
189
181
  listProperties(): Promise<PropertyListEntry[]>;
190
182
  listTags(): Promise<TagListEntry[]>;
package/dist/cli.js CHANGED
@@ -14180,6 +14180,22 @@ function coerceFieldValue(schema, value, fieldName = "value") {
14180
14180
  }
14181
14181
  return value;
14182
14182
  }
14183
+ if (inner instanceof external_exports.ZodArray && typeof value === "string") {
14184
+ let parsed;
14185
+ try {
14186
+ parsed = JSON.parse(value);
14187
+ } catch {
14188
+ throw new CoerceError(
14189
+ fieldName,
14190
+ `expected array or JSON-string of one, failed to parse: ${JSON.stringify(value)}`
14191
+ );
14192
+ }
14193
+ if (Array.isArray(parsed)) return parsed;
14194
+ throw new CoerceError(
14195
+ fieldName,
14196
+ `expected array, parsed JSON resolved to ${describeJsonShape(parsed)}`
14197
+ );
14198
+ }
14183
14199
  return value;
14184
14200
  }
14185
14201
  function wrapField(field, fieldName) {
@@ -14530,7 +14546,7 @@ function normalizeScanPrefix(raw) {
14530
14546
  }
14531
14547
 
14532
14548
  // src/lib/obsidian/frontmatter.ts
14533
- import { parse as parseYaml } from "yaml";
14549
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
14534
14550
 
14535
14551
  // src/lib/obsidian/in-place-edit.ts
14536
14552
  var FRONTMATTER_OPEN = /^---[ \t]*\r?\n/;
@@ -14611,6 +14627,11 @@ function splitFrontmatter(raw) {
14611
14627
  }
14612
14628
  return { frontmatter: parsed, content };
14613
14629
  }
14630
+ function serializeFrontmatter(fm) {
14631
+ return `---
14632
+ ${stringifyYaml(fm)}---
14633
+ `;
14634
+ }
14614
14635
 
14615
14636
  // src/lib/obsidian/wikilink.ts
14616
14637
  var WIKILINK_RE = /\[\[(.+?)\]\]/g;
@@ -15762,70 +15783,6 @@ function buildGetSimilarNotesTool(deps) {
15762
15783
  };
15763
15784
  }
15764
15785
 
15765
- // src/modules/semantic/tools/get-stats.ts
15766
- function readEmbeddingDimension(sources) {
15767
- let dimension;
15768
- for (const source of sources) {
15769
- if (dimension === void 0) {
15770
- dimension = source.embedding.length;
15771
- continue;
15772
- }
15773
- if (source.embedding.length !== dimension) {
15774
- throw new ToolHandlerError(
15775
- "DEPENDENCY_ERROR",
15776
- "Loaded corpus contains mixed embedding dimensions",
15777
- {
15778
- details: {
15779
- expectedDimension: dimension,
15780
- actualDimension: source.embedding.length,
15781
- path: source.path
15782
- }
15783
- }
15784
- );
15785
- }
15786
- }
15787
- return dimension ?? 0;
15788
- }
15789
- function buildGetStatsTool(deps) {
15790
- const { registry: registry2, modelKey } = deps;
15791
- const inputSchema = external_exports.object({ ...vaultParamShape(registry2) });
15792
- return {
15793
- name: "get_stats",
15794
- title: "Get Stats",
15795
- description: "Report corpus and embedding statistics." + describeMultiVault(
15796
- registry2,
15797
- 'Pass `vault: "<name>"` to target a specific vault when multiple are registered.'
15798
- ),
15799
- inputSchema,
15800
- handler: async (input) => {
15801
- const entry = resolveSemanticVault(input, registry2, {
15802
- tool: "get_stats"
15803
- });
15804
- const corpus = entry.corpus;
15805
- try {
15806
- const { sources } = await corpus.snapshot();
15807
- let totalBlocks = 0;
15808
- for (const source of sources.values()) {
15809
- totalBlocks += source.blocks.length;
15810
- }
15811
- return {
15812
- vault: entry.name,
15813
- totalNotes: sources.size,
15814
- totalBlocks,
15815
- embeddingDimension: readEmbeddingDimension(sources.values()),
15816
- modelKey
15817
- };
15818
- } catch (error48) {
15819
- if (error48 instanceof ToolHandlerError) throw error48;
15820
- throw new ToolHandlerError("DEPENDENCY_ERROR", "Failed to get corpus stats", {
15821
- details: { modelKey, operation: "get_stats" },
15822
- cause: error48
15823
- });
15824
- }
15825
- }
15826
- };
15827
- }
15828
-
15829
15786
  // src/lib/fan-out.ts
15830
15787
  function findFatalRejection(settled) {
15831
15788
  for (const outcome of settled) {
@@ -16354,8 +16311,7 @@ function buildSemanticTools(deps) {
16354
16311
  return [
16355
16312
  registerTool(buildSearchNotesTool(deps)),
16356
16313
  registerTool(buildGetSimilarNotesTool(deps)),
16357
- registerTool(buildFindDuplicatesTool(deps)),
16358
- registerTool(buildGetStatsTool(deps))
16314
+ registerTool(buildFindDuplicatesTool(deps))
16359
16315
  ];
16360
16316
  }
16361
16317
 
@@ -16472,8 +16428,7 @@ function normalizePath(raw) {
16472
16428
  throw invalidArgument(err.message, "path");
16473
16429
  }
16474
16430
  }
16475
- var VALID_FIELDS = ["frontmatter", "content"];
16476
- var DEFAULT_FIELDS = ["frontmatter", "content"];
16431
+ var VALID_CONTENT_MODES = ["full", "preview", "frontmatter"];
16477
16432
  function validateReadNotesInput(input) {
16478
16433
  let paths;
16479
16434
  if (typeof input.paths === "string") {
@@ -16489,24 +16444,18 @@ function validateReadNotesInput(input) {
16489
16444
  if (paths.length < 1 || paths.length > 50) {
16490
16445
  throw invalidArgument("paths must contain between 1 and 50 entries", "paths");
16491
16446
  }
16492
- let fields;
16493
- if (input.fields === void 0) {
16494
- fields = DEFAULT_FIELDS;
16447
+ let content;
16448
+ if (input.content === void 0) {
16449
+ content = void 0;
16450
+ } else if (VALID_CONTENT_MODES.includes(input.content)) {
16451
+ content = input.content;
16495
16452
  } else {
16496
- if (!Array.isArray(input.fields) || input.fields.length === 0) {
16497
- throw invalidArgument("fields must be a non-empty array when provided", "fields");
16498
- }
16499
- for (const f of input.fields) {
16500
- if (!VALID_FIELDS.includes(f)) {
16501
- throw invalidArgument(
16502
- `unknown field '${String(f)}'; allowed: ${VALID_FIELDS.join(", ")}`,
16503
- "fields"
16504
- );
16505
- }
16506
- }
16507
- fields = input.fields;
16453
+ throw invalidArgument(
16454
+ `unknown content mode '${String(input.content)}'; allowed: ${VALID_CONTENT_MODES.join(", ")}`,
16455
+ "content"
16456
+ );
16508
16457
  }
16509
- return { paths, fields };
16458
+ return { paths, content };
16510
16459
  }
16511
16460
 
16512
16461
  // src/modules/operations/tools/create-note.ts
@@ -16517,12 +16466,13 @@ function buildCreateNoteTool(deps) {
16517
16466
  name: external_exports.string().optional(),
16518
16467
  path: external_exports.string().optional(),
16519
16468
  content: external_exports.string().optional(),
16469
+ frontmatter: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
16520
16470
  overwrite: external_exports.boolean().optional()
16521
16471
  });
16522
16472
  return {
16523
16473
  name: "create_note",
16524
16474
  title: "Create Note",
16525
- description: "Create a new note. Provide `name` or `path` (exactly one). Optionally provide `content` (raw markdown for the note body and frontmatter). Paths without an extension are treated as `.md` notes.\n\nBefore composing `content`, sample 1\u20132 similar notes from the vault to mimic existing conventions instead of inventing your own. A reliable pattern: `search_notes` for the topic (or `query_notes` with a tag/folder filter that fits) to find candidates, then `read_notes` on the closest match to inspect its frontmatter shape, tag values, heading layout, and folder placement. Match those conventions \u2014 the user almost always prefers a new note that looks like its neighbours. Be especially careful with the `type` frontmatter field: vaults tend to use a small closed set (e.g. project / task / idea / reflection / daily / review / inbox / resource); pick from what other notes use rather than coining a new value.\n\nTemplates are not handled by this tool \u2014 render any template yourself (Obsidian Core Templates, Templater, or anything else) and pass the result as `content`." + describeMultiVault(
16475
+ description: "Create a new note. Provide `name` or `path` (exactly one). Optionally provide `content` (raw markdown for the note body) and/or `frontmatter` (an object of frontmatter properties, serialized to a YAML block and prepended to the note \u2014 prefer this over hand-writing a `---` block inside `content`; it quotes `[[wikilinks]]`, formats dates, and renders tag lists correctly). If `content` also begins with its own `---` block, the two are merged key-by-key and the `frontmatter` parameter wins on any key collision (the body is kept). Paths without an extension are treated as `.md` notes.\n\nBefore composing `content`, sample 1\u20132 similar notes from the vault to mimic existing conventions instead of inventing your own. A reliable pattern: `search_notes` for the topic (or `query_notes` with a tag/folder filter that fits) to find candidates, then `read_notes` on the closest match to inspect its frontmatter shape, tag values, heading layout, and folder placement. Match those conventions \u2014 the user almost always prefers a new note that looks like its neighbours. Be especially careful with the `type` frontmatter field: vaults tend to use a small closed set (e.g. project / task / idea / reflection / daily / review / inbox / resource); pick from what other notes use rather than coining a new value.\n\nTemplates are not handled by this tool \u2014 render any template yourself (Obsidian Core Templates, Templater, or anything else) and pass the result as `content`." + describeMultiVault(
16526
16476
  registry2,
16527
16477
  'Pass `vault: "<name>"` to target a specific vault when multiple are registered.'
16528
16478
  ) + " If a note with this path/name might already exist and the user has not explicitly asked to replace it, ask the user before passing `overwrite: true` \u2014 overwrite is destructive. Default behavior fails when the note exists.",
@@ -16548,7 +16498,14 @@ function buildCreateNoteTool(deps) {
16548
16498
  }
16549
16499
  }
16550
16500
  if (input.overwrite !== void 0) passthrough.overwrite = input.overwrite;
16551
- if (input.content !== void 0) passthrough.content = input.content;
16501
+ const hasFrontmatter = input.frontmatter !== void 0 && Object.keys(input.frontmatter).length > 0;
16502
+ if (hasFrontmatter) {
16503
+ const { frontmatter: contentFm, content: body } = splitFrontmatter(input.content ?? "");
16504
+ const merged = { ...contentFm ?? {}, ...input.frontmatter };
16505
+ passthrough.content = serializeFrontmatter(merged) + body;
16506
+ } else if (input.content !== void 0) {
16507
+ passthrough.content = input.content;
16508
+ }
16552
16509
  const result = await entry.provider.createNote(passthrough);
16553
16510
  return { vault: entry.name, ...result };
16554
16511
  }
@@ -16739,7 +16696,7 @@ function buildGetVaultOverviewTool(deps) {
16739
16696
  return {
16740
16697
  name: "get_vault_overview",
16741
16698
  title: "Get Vault Overview",
16742
- description: "Returns a single snapshot of vault structure: top-level folders with note counts, top tags, frontmatter properties, total note count, and the top 10 notes by inbound wikilinks. Call this once at the start of a session to orient yourself before reaching for `list_tags`, `list_properties`, or exploratory `query_notes`." + describeMultiVault(
16699
+ description: "Returns a single snapshot of vault structure: top-level folders with note counts, top tags, frontmatter properties, total note count, and the top 10 notes by inbound wikilinks. Call this once at the start of a session to orient yourself before reaching for `list_tags` or exploratory `query_notes`." + describeMultiVault(
16743
16700
  registry2,
16744
16701
  'In multi-vault mode, omit `vault:` to fan out across all registered vaults \u2014 the response shape switches to `results_by_vault: [...]` with `skipped_vaults: [...]`. Pass `vault: "<name>"` to target a specific vault when multiple are registered.'
16745
16702
  ),
@@ -16755,35 +16712,8 @@ function buildGetVaultOverviewTool(deps) {
16755
16712
  };
16756
16713
  }
16757
16714
 
16758
- // src/modules/operations/tools/list-properties.ts
16759
- async function runForEntry(entry) {
16760
- const results = await entry.provider.listProperties();
16761
- return { results };
16762
- }
16763
- function buildListPropertiesTool(deps) {
16764
- const { registry: registry2 } = deps;
16765
- const inputSchema = external_exports.object({ ...vaultParamShape(registry2) });
16766
- return {
16767
- name: "list_properties",
16768
- title: "List Properties",
16769
- description: "List all frontmatter properties used across the vault, sorted by occurrence count desc. Returns `{ vault, results: [{name, count}] }`. Useful for understanding the vault's metadata ontology." + describeMultiVault(
16770
- registry2,
16771
- 'In multi-vault mode, omit `vault:` to fan out across all registered vaults \u2014 the response shape switches to `results_by_vault: [...]`. Pass `vault: "<name>"` to target a specific vault.'
16772
- ),
16773
- inputSchema,
16774
- handler: async (input) => {
16775
- if (input.vault === void 0 && registry2.isMulti()) {
16776
- return await runFanOut(registry2, runForEntry);
16777
- }
16778
- const entry = resolveVault(input, registry2, { tool: "list_properties" });
16779
- const { results } = await runForEntry(entry);
16780
- return { vault: entry.name, results };
16781
- }
16782
- };
16783
- }
16784
-
16785
16715
  // src/modules/operations/tools/list-tags.ts
16786
- async function runForEntry2(entry) {
16716
+ async function runForEntry(entry) {
16787
16717
  const results = await entry.provider.listTags();
16788
16718
  return { results };
16789
16719
  }
@@ -16800,10 +16730,10 @@ function buildListTagsTool(deps) {
16800
16730
  inputSchema,
16801
16731
  handler: async (input) => {
16802
16732
  if (input.vault === void 0 && registry2.isMulti()) {
16803
- return await runFanOut(registry2, runForEntry2);
16733
+ return await runFanOut(registry2, runForEntry);
16804
16734
  }
16805
16735
  const entry = resolveVault(input, registry2, { tool: "list_tags" });
16806
- const { results } = await runForEntry2(entry);
16736
+ const { results } = await runForEntry(entry);
16807
16737
  return { vault: entry.name, results };
16808
16738
  }
16809
16739
  };
@@ -16953,26 +16883,39 @@ function formatLocalDate(d) {
16953
16883
  return `${y}-${m}-${day}`;
16954
16884
  }
16955
16885
 
16886
+ // src/modules/operations/preview-body.ts
16887
+ var PREVIEW_CHAR_CAP = 500;
16888
+ var PREVIEW_MARKER = "\u2026";
16889
+ function previewBody(body) {
16890
+ if (body.length <= PREVIEW_CHAR_CAP) {
16891
+ return { content: body, truncated: false };
16892
+ }
16893
+ const segment = body.slice(0, PREVIEW_CHAR_CAP + 1);
16894
+ const lastWs = Math.max(segment.lastIndexOf(" "), segment.lastIndexOf("\n"));
16895
+ const cutAt = lastWs !== -1 ? lastWs : PREVIEW_CHAR_CAP;
16896
+ const slice = body.slice(0, cutAt).trimEnd();
16897
+ return { content: slice + PREVIEW_MARKER, truncated: true };
16898
+ }
16899
+
16956
16900
  // src/modules/operations/tools/read-notes.ts
16957
- var readNotesFieldSchema = external_exports.enum(["frontmatter", "content"]);
16958
16901
  function buildReadNotesTool(deps) {
16959
16902
  const { registry: registry2 } = deps;
16960
16903
  const inputSchema = external_exports.object({
16961
16904
  ...vaultParamShape(registry2),
16962
16905
  paths: external_exports.union([external_exports.string().min(1), external_exports.array(external_exports.string()).min(1).max(50)]),
16963
- fields: external_exports.array(readNotesFieldSchema).min(1).optional()
16906
+ content: external_exports.enum(["full", "preview", "frontmatter"]).optional()
16964
16907
  });
16965
16908
  return {
16966
16909
  name: "read_notes",
16967
16910
  title: "Read Notes",
16968
- description: "Read one or more notes in one call. `paths` is a vault-relative POSIX path string or an array of 1\u201350 such paths; duplicates are de-duplicated and results returned in input order. `fields` projects which parts of each note to return \u2014 choose from `frontmatter` and `content`; default `['frontmatter','content']`. One missing or unreadable path does not fail the others \u2014 per-item errors come back inline. A single MCP roundtrip with parallel disk reads. Reads are direct from disk and do not require Obsidian to be running." + describeMultiVault(
16911
+ description: "Read one or more notes in one call. `paths` is a vault-relative POSIX path string or an array of 1\u201350 such paths; duplicates are de-duplicated and results returned in input order. `content` controls how much of each note's body comes back: `full` returns the complete body, `preview` returns a bounded slice plus a `truncated` flag, `frontmatter` returns no body at all. Frontmatter is always returned. The default is derived from the number of distinct paths: one path \u2192 `full`, two or more \u2192 `preview`; passing `content` explicitly overrides this. Re-read a previewed note with `content: 'full'` before citing or editing it. One missing or unreadable path does not fail the others \u2014 per-item errors come back inline. A single MCP roundtrip with parallel disk reads. Reads are direct from disk and do not require Obsidian to be running." + describeMultiVault(
16969
16912
  registry2,
16970
16913
  'Pass `vault: "<name>"` to target a specific vault when multiple are registered.'
16971
16914
  ),
16972
16915
  inputSchema,
16973
16916
  handler: async (input) => {
16974
16917
  const entry = resolveVault(input, registry2, { tool: "read_notes" });
16975
- const { paths, fields } = validateReadNotesInput(input);
16918
+ const { paths, content } = validateReadNotesInput(input);
16976
16919
  const seen = /* @__PURE__ */ new Set();
16977
16920
  const deduped = [];
16978
16921
  for (const p of paths) {
@@ -16981,6 +16924,8 @@ function buildReadNotesTool(deps) {
16981
16924
  deduped.push(p);
16982
16925
  }
16983
16926
  }
16927
+ const effective = content ?? (deduped.length === 1 ? "full" : "preview");
16928
+ const fields = effective === "frontmatter" ? ["frontmatter"] : ["frontmatter", "content"];
16984
16929
  const slots = deduped.map((raw) => {
16985
16930
  try {
16986
16931
  const normalized = normalizePath(raw);
@@ -17000,13 +16945,15 @@ function buildReadNotesTool(deps) {
17000
16945
  return item;
17001
16946
  }
17002
16947
  const out = {
17003
- path: item.path
16948
+ path: item.path,
16949
+ frontmatter: item.frontmatter
17004
16950
  };
17005
- if (fields.includes("frontmatter")) {
17006
- out.frontmatter = item.frontmatter;
17007
- }
17008
- if (fields.includes("content")) {
16951
+ if (effective === "full") {
17009
16952
  out.content = item.content;
16953
+ } else if (effective === "preview") {
16954
+ const { content: c, truncated } = previewBody(item.content);
16955
+ out.content = c;
16956
+ out.truncated = truncated;
17010
16957
  }
17011
16958
  return out;
17012
16959
  });
@@ -17021,35 +16968,6 @@ function buildReadNotesTool(deps) {
17021
16968
  };
17022
16969
  }
17023
16970
 
17024
- // src/modules/operations/tools/read-property.ts
17025
- function buildReadPropertyTool(deps) {
17026
- const { registry: registry2 } = deps;
17027
- const inputSchema = external_exports.object({
17028
- ...vaultParamShape(registry2),
17029
- name: external_exports.string().optional(),
17030
- path: external_exports.string().optional(),
17031
- key: external_exports.string()
17032
- });
17033
- return {
17034
- name: "read_property",
17035
- title: "Read Property",
17036
- description: "Read a frontmatter property value from a note. Provide `name` or `path`, plus `key`. Returns `{ vault, value }`. Use `read_notes` if you need the full frontmatter or accurate type information." + describeMultiVault(
17037
- registry2,
17038
- 'Pass `vault: "<name>"` to target a specific vault when multiple are registered.'
17039
- ),
17040
- inputSchema,
17041
- handler: async (input) => {
17042
- const entry = resolveVault(input, registry2, { tool: "read_property" });
17043
- const identifier = resolveIdentifier(input.name, input.path);
17044
- if (!input.key || input.key.trim() === "") {
17045
- throw invalidArgument("key must not be empty", "key");
17046
- }
17047
- const result = await entry.provider.readProperty({ identifier, name: input.key.trim() });
17048
- return { vault: entry.name, ...result };
17049
- }
17050
- };
17051
- }
17052
-
17053
16971
  // src/modules/operations/tools/remove-property.ts
17054
16972
  function buildRemovePropertyTool(deps) {
17055
16973
  const { registry: registry2 } = deps;
@@ -17120,9 +17038,7 @@ function buildOperationsTools(deps) {
17120
17038
  registerTool(buildEditNoteTool(deps)),
17121
17039
  registerTool(buildReadDailyTool(deps)),
17122
17040
  registerTool(buildSetPropertyTool(deps)),
17123
- registerTool(buildReadPropertyTool(deps)),
17124
17041
  registerTool(buildRemovePropertyTool(deps)),
17125
- registerTool(buildListPropertiesTool(deps)),
17126
17042
  registerTool(buildListTagsTool(deps)),
17127
17043
  registerTool(buildGetNoteLinksTool(deps)),
17128
17044
  registerTool(buildGetVaultOverviewTool(deps))
@@ -17423,13 +17339,6 @@ var ObsidianCLIProvider = class {
17423
17339
  tokens.push(identifierToArg(input.identifier));
17424
17340
  await this.runCommand("property:set", tokens);
17425
17341
  }
17426
- async readProperty(input) {
17427
- const { stdout } = await this.runCommand("property:read", [
17428
- `name=${input.name}`,
17429
- identifierToArg(input.identifier)
17430
- ]);
17431
- return { value: this.parsePropertyValue(stdout) };
17432
- }
17433
17342
  async removeProperty(input) {
17434
17343
  try {
17435
17344
  await this.runCommand("property:remove", [
@@ -17451,20 +17360,6 @@ var ObsidianCLIProvider = class {
17451
17360
  const { stdout } = await this.runCommand("tags", ["counts", "sort=count", "format=json"]);
17452
17361
  return this.parseJsonList(stdout, "tags");
17453
17362
  }
17454
- // Best-effort: a `text` property whose value happens to be "true" or "42"
17455
- // will be coerced to boolean/number. Callers needing ground-truth types should
17456
- // use read_notes and parse frontmatter directly.
17457
- parsePropertyValue(stdout) {
17458
- const trimmed = stdout.trim();
17459
- if (trimmed === "") return "";
17460
- if (trimmed === "true") return true;
17461
- if (trimmed === "false") return false;
17462
- if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
17463
- if (trimmed.includes("\n")) {
17464
- return trimmed.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
17465
- }
17466
- return trimmed;
17467
- }
17468
17363
  serializeValue(value) {
17469
17364
  if (Array.isArray(value)) return value.map((v) => String(v)).join(",");
17470
17365
  return String(value);
@@ -17533,7 +17428,7 @@ var ObsidianCLIProvider = class {
17533
17428
  { details: { stderr }, cause: error48 }
17534
17429
  );
17535
17430
  }
17536
- if ((command === "property:read" || command === "property:remove") && /property not found|not set/i.test(stderr)) {
17431
+ if (command === "property:remove" && /property not found|not set/i.test(stderr)) {
17537
17432
  return new ToolHandlerError(
17538
17433
  "PROPERTY_NOT_FOUND",
17539
17434
  `Property not found: ${stderr.trim() || "unknown"}`,
@@ -17641,7 +17536,7 @@ This vault is the user's personal knowledge base \u2014 planning notes, brainsto
17641
17536
 
17642
17537
  You don't know on your own how the user scopes notes for the current project \u2014 the same vault is typically connected to many, and different users organise differently (a tag, a folder, a frontmatter property like \`project\`, or a combination). The user's project-level instructions should name that scope and tell you how to query for it. If they don't, you have three ways forward, in order of preference:
17643
17538
 
17644
- 1. **Probe the vault structure** via \`get_vault_overview\` \u2014 a single-call snapshot of top-level folders with counts, top tags, frontmatter properties with inferred types, and the top-linked notes. Common conventions (a \`project\` frontmatter field, a \`Projects/\` folder, a recurring tag) usually surface immediately. Fall back to \`list_tags\`, \`list_properties\`, or exploratory \`query_notes\` only when the overview misses what you need.
17539
+ 1. **Probe the vault structure** via \`get_vault_overview\` \u2014 a single-call snapshot of top-level folders with counts, top tags, frontmatter properties with inferred types, and the top-linked notes. Common conventions (a \`project\` frontmatter field, a \`Projects/\` folder, a recurring tag) usually surface immediately. Fall back to \`list_tags\` or exploratory \`query_notes\` only when the overview misses what you need.
17645
17540
  2. **Use \`search_notes\`** with the project name and key concepts as a fuzzy entry point \u2014 relevant material may exist under unrelated names.
17646
17541
  3. **Ask the user**, and the first time they save project-specific notes propose a scoping scheme so future sessions have an explicit entry point.
17647
17542
 
@@ -17665,11 +17560,11 @@ Use \`query_notes\` for multi-criteria questions that combine tags, frontmatter
17665
17560
 
17666
17561
  ### Frontmatter properties
17667
17562
 
17668
- Use \`set_property\`, \`read_property\`, \`remove_property\` when the user asks to read or modify a single YAML frontmatter field (status, due date, priority, etc.). Use \`list_properties\` to see what property names are already in use across the vault \u2014 useful before introducing a new one.
17563
+ Use \`set_property\`, \`remove_property\` when the user asks to set or remove a single YAML frontmatter field (status, due date, priority, etc.).
17669
17564
 
17670
17565
  \`set_property\` infers \`type\` from the JS value (string\u2192text, number\u2192number, boolean\u2192checkbox, array\u2192list). For \`date\`/\`datetime\` you MUST pass \`type\` explicitly AND use ISO format (\`YYYY-MM-DD\` or \`YYYY-MM-DDTHH:mm:ss[.sss][Z|\xB1HH:mm]\`) \u2014 non-ISO values are silently dropped by the CLI, so the tool rejects them up front. Existing values are overwritten without asking.
17671
17566
 
17672
- If you need frontmatter for one or more notes, call \`read_notes\` with \`fields: ['frontmatter']\` \u2014 that single batch call replaces N \`read_property\` calls when you have a list of paths.
17567
+ To read a single frontmatter value, or the full frontmatter of one or more notes, call \`read_notes\` with \`content: 'frontmatter'\` \u2014 a single batch call returns each note's frontmatter object with accurate types.
17673
17568
 
17674
17569
  ### Tags
17675
17570
 
@@ -17718,7 +17613,7 @@ For tag-driven questions ("which notes are tagged X?", "show me everything in #a
17718
17613
  `;
17719
17614
  var GET_VAULT_OVERVIEW_HINT = `## Orientation
17720
17615
 
17721
- Before reaching for \`list_tags\`, \`list_properties\`, or exploratory \`query_notes\`, call \`get_vault_overview\` once at the start of a session. It returns the top-level folder layout with counts, the top tags, frontmatter properties with inferred types, the total note count, and the top 10 notes by inbound wikilinks \u2014 enough to orient yourself in a single call. The same payload is available as the MCP resource \`vault://overview\` for clients that auto-load resources.`;
17616
+ Before reaching for \`list_tags\` or exploratory \`query_notes\`, call \`get_vault_overview\` once at the start of a session. It returns the top-level folder layout with counts, the top tags, frontmatter properties with inferred types, the total note count, and the top 10 notes by inbound wikilinks \u2014 enough to orient yourself in a single call. The same payload is available as the MCP resource \`vault://overview\` for clients that auto-load resources.`;
17722
17617
  async function buildServerInstructions(registry2) {
17723
17618
  let result = STATIC_SERVER_INSTRUCTIONS;
17724
17619
  result += "\n\n" + GET_VAULT_OVERVIEW_HINT;
@@ -17728,7 +17623,7 @@ async function buildServerInstructions(registry2) {
17728
17623
 
17729
17624
  ## Multi-vault mode
17730
17625
 
17731
- This server is registered with multiple vaults: ${names}. Every tool accepts an optional \`vault\` parameter. For broad recall, \`search_notes\`, \`query_notes\`, \`get_vault_overview\`, \`list_tags\`, and \`list_properties\` fan out across all vaults when \`vault\` is omitted; other tools (reads of specific paths, writes, single-vault diagnostics) require an explicit \`vault\` \u2014 omitting it returns \`VAULT_REQUIRED\`. Fan-out responses include \`results_by_vault\`, \`skipped_vaults\` (vaults pre-filtered out, e.g. missing semantic index), and \`failed_vaults\` (per-vault runtime errors with \`{ code, message, details? }\`) \u2014 a single failed vault does not abort the whole call. Path-shaped parameters (\`path\`, \`paths\`, \`path_prefix\`) remain vault-relative; vault identity is always carried by \`vault\`.`;
17626
+ This server is registered with multiple vaults: ${names}. Every tool accepts an optional \`vault\` parameter. For broad recall, \`search_notes\`, \`query_notes\`, \`get_vault_overview\`, and \`list_tags\` fan out across all vaults when \`vault\` is omitted; other tools (reads of specific paths, writes, single-vault diagnostics) require an explicit \`vault\` \u2014 omitting it returns \`VAULT_REQUIRED\`. Fan-out responses include \`results_by_vault\`, \`skipped_vaults\` (vaults pre-filtered out, e.g. missing semantic index), and \`failed_vaults\` (per-vault runtime errors with \`{ code, message, details? }\`) \u2014 a single failed vault does not abort the whole call. Path-shaped parameters (\`path\`, \`paths\`, \`path_prefix\`) remain vault-relative; vault identity is always carried by \`vault\`.`;
17732
17627
  }
17733
17628
  for (const entry of registry2.list()) {
17734
17629
  const extra = await readExternalAgentInstructions(entry.path);