neuro-vault-mcp 8.0.0 → 8.2.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 +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +158 -56
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ One question, one answer. Your assistant stops being a file browser and starts b
|
|
|
72
72
|
{ "query": "trading lessons", "filter": { "tags": ["trading"] } }
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
`filter` accepts `path_prefix
|
|
75
|
+
`filter` accepts `path_prefix` (string or array), `exclude_path_prefix` (string or array — drops matched subtrees), `tags` (ANY-of), and a `frontmatter` sift filter. Composition is include → exclude → tags → frontmatter → threshold → semantic. See the [Semantic Search guide](./docs/guide/semantic-search.md#pre-filter-filter-parameter) for full details.
|
|
76
76
|
|
|
77
77
|
---
|
|
78
78
|
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -15202,6 +15202,56 @@ function normalizeVaultPathPrefix(raw, invalid) {
|
|
|
15202
15202
|
return slashed.replace(/\/+$/, "");
|
|
15203
15203
|
}
|
|
15204
15204
|
|
|
15205
|
+
// src/lib/obsidian/query/path-prefix-set.ts
|
|
15206
|
+
function normalizePrefixList(raw, field, errorCode) {
|
|
15207
|
+
if (raw === void 0) return void 0;
|
|
15208
|
+
const invalid = (message) => new ToolHandlerError(errorCode, message, { details: { field } });
|
|
15209
|
+
if (Array.isArray(raw)) {
|
|
15210
|
+
if (raw.length === 0) {
|
|
15211
|
+
throw invalid(`${field} must contain at least one prefix`);
|
|
15212
|
+
}
|
|
15213
|
+
const out = /* @__PURE__ */ new Set();
|
|
15214
|
+
for (const entry of raw) {
|
|
15215
|
+
if (typeof entry !== "string") {
|
|
15216
|
+
throw invalid(`${field} elements must be strings`);
|
|
15217
|
+
}
|
|
15218
|
+
const normalized2 = normalizeVaultPathPrefix(entry, invalid);
|
|
15219
|
+
if (normalized2 !== void 0) out.add(normalized2);
|
|
15220
|
+
}
|
|
15221
|
+
return out.size === 0 ? void 0 : [...out];
|
|
15222
|
+
}
|
|
15223
|
+
if (typeof raw !== "string") {
|
|
15224
|
+
throw invalid(`${field} must be a string or array of strings`);
|
|
15225
|
+
}
|
|
15226
|
+
const normalized = normalizeVaultPathPrefix(raw, invalid);
|
|
15227
|
+
return normalized === void 0 ? void 0 : [normalized];
|
|
15228
|
+
}
|
|
15229
|
+
function matchesAnyPrefix(p, prefixes) {
|
|
15230
|
+
for (const q of prefixes) {
|
|
15231
|
+
if (p === q) return true;
|
|
15232
|
+
if (p.startsWith(`${q}/`)) return true;
|
|
15233
|
+
}
|
|
15234
|
+
return false;
|
|
15235
|
+
}
|
|
15236
|
+
function rethrowPathNotFoundWithIndex(err, prefix, index, total) {
|
|
15237
|
+
const isRaw = err instanceof ScanPathNotFoundError;
|
|
15238
|
+
const isWrapped = err instanceof ToolHandlerError && err.code === "PATH_NOT_FOUND";
|
|
15239
|
+
if (!isRaw && !isWrapped) {
|
|
15240
|
+
throw err;
|
|
15241
|
+
}
|
|
15242
|
+
if (total <= 1) {
|
|
15243
|
+
if (isRaw) {
|
|
15244
|
+
throw new ToolHandlerError("PATH_NOT_FOUND", err.message);
|
|
15245
|
+
}
|
|
15246
|
+
throw err;
|
|
15247
|
+
}
|
|
15248
|
+
throw new ToolHandlerError(
|
|
15249
|
+
"PATH_NOT_FOUND",
|
|
15250
|
+
`path_prefix[${index}] not found: ${JSON.stringify(prefix)}`,
|
|
15251
|
+
{ details: { path_prefix: prefix, index } }
|
|
15252
|
+
);
|
|
15253
|
+
}
|
|
15254
|
+
|
|
15205
15255
|
// src/lib/obsidian/query/whitelist.ts
|
|
15206
15256
|
var ALLOWED_OPERATORS = /* @__PURE__ */ new Set([
|
|
15207
15257
|
"$eq",
|
|
@@ -15289,6 +15339,9 @@ async function collectMatchingPaths(input, deps) {
|
|
|
15289
15339
|
}
|
|
15290
15340
|
continue;
|
|
15291
15341
|
}
|
|
15342
|
+
if (input.excludePathPrefixes !== void 0 && matchesAnyPrefix(item.path, input.excludePathPrefixes)) {
|
|
15343
|
+
continue;
|
|
15344
|
+
}
|
|
15292
15345
|
const backlinkCount = graph?.getBacklinkCount(item.path) ?? 0;
|
|
15293
15346
|
const record2 = toNoteRecord(item, backlinkCount);
|
|
15294
15347
|
let isMatch;
|
|
@@ -15312,16 +15365,37 @@ async function runQueryNotes(input, reader, graph) {
|
|
|
15312
15365
|
if (graph) {
|
|
15313
15366
|
await graph.ensureFresh();
|
|
15314
15367
|
}
|
|
15315
|
-
const
|
|
15316
|
-
const
|
|
15317
|
-
|
|
15318
|
-
|
|
15319
|
-
|
|
15320
|
-
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
|
|
15368
|
+
const sortOrderMatchesScan = !validated.sort || validated.sort.field === "path" && validated.sort.order === "asc";
|
|
15369
|
+
const includes = validated.includePrefixes ?? [void 0];
|
|
15370
|
+
const multiPrefix = includes.length > 1;
|
|
15371
|
+
const earlyExitAfter = sortOrderMatchesScan && !multiPrefix ? validated.limit : void 0;
|
|
15372
|
+
const totalIncludes = includes.length;
|
|
15373
|
+
const batches = await Promise.all(
|
|
15374
|
+
includes.map((prefix, i) => {
|
|
15375
|
+
const promise2 = collectMatchingPaths(
|
|
15376
|
+
{
|
|
15377
|
+
filter: effectiveFilter,
|
|
15378
|
+
pathPrefix: prefix,
|
|
15379
|
+
includeContent: validated.includeContent,
|
|
15380
|
+
earlyExitAfter,
|
|
15381
|
+
excludePathPrefixes: validated.excludePrefixes
|
|
15382
|
+
},
|
|
15383
|
+
{ reader, graph }
|
|
15384
|
+
);
|
|
15385
|
+
return prefix === void 0 ? promise2 : promise2.catch(
|
|
15386
|
+
(err) => rethrowPathNotFoundWithIndex(err, prefix, i, totalIncludes)
|
|
15387
|
+
);
|
|
15388
|
+
})
|
|
15324
15389
|
);
|
|
15390
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15391
|
+
const matched = [];
|
|
15392
|
+
for (const rows of batches) {
|
|
15393
|
+
for (const row of rows) {
|
|
15394
|
+
if (seen.has(row.record.path)) continue;
|
|
15395
|
+
seen.add(row.record.path);
|
|
15396
|
+
matched.push(row);
|
|
15397
|
+
}
|
|
15398
|
+
}
|
|
15325
15399
|
if (validated.sort) {
|
|
15326
15400
|
sortInPlace(matched, validated.sort);
|
|
15327
15401
|
}
|
|
@@ -15351,13 +15425,12 @@ function validateInput(input) {
|
|
|
15351
15425
|
if (typeof input.filter !== "object" || Array.isArray(input.filter)) {
|
|
15352
15426
|
throw invalidParams("filter must be a JSON object", "filter");
|
|
15353
15427
|
}
|
|
15354
|
-
|
|
15355
|
-
|
|
15356
|
-
|
|
15357
|
-
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
}
|
|
15428
|
+
const includePrefixes = normalizePrefixList(input.path_prefix, "path_prefix", "INVALID_PARAMS");
|
|
15429
|
+
const excludePrefixes = normalizePrefixList(
|
|
15430
|
+
input.exclude_path_prefix,
|
|
15431
|
+
"exclude_path_prefix",
|
|
15432
|
+
"INVALID_PARAMS"
|
|
15433
|
+
);
|
|
15361
15434
|
let sort;
|
|
15362
15435
|
if (input.sort !== void 0) {
|
|
15363
15436
|
sort = validateSort(input.sort);
|
|
@@ -15378,15 +15451,13 @@ function validateInput(input) {
|
|
|
15378
15451
|
}
|
|
15379
15452
|
return {
|
|
15380
15453
|
filter: input.filter,
|
|
15381
|
-
|
|
15454
|
+
includePrefixes,
|
|
15455
|
+
excludePrefixes,
|
|
15382
15456
|
sort,
|
|
15383
15457
|
limit,
|
|
15384
15458
|
includeContent
|
|
15385
15459
|
};
|
|
15386
15460
|
}
|
|
15387
|
-
function normalizeScanPrefixInput(raw) {
|
|
15388
|
-
return normalizeVaultPathPrefix(raw, (message) => invalidParams(message, "path_prefix"));
|
|
15389
|
-
}
|
|
15390
15461
|
function validateSort(raw) {
|
|
15391
15462
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
15392
15463
|
throw invalidParams("sort must be an object { field, order }", "sort");
|
|
@@ -15448,47 +15519,71 @@ function invalidParams(message, field) {
|
|
|
15448
15519
|
// src/lib/obsidian/query/list-matching-paths.ts
|
|
15449
15520
|
function createListMatchingPaths(deps) {
|
|
15450
15521
|
return async (filter) => {
|
|
15451
|
-
const { path_prefix, tags, frontmatter } = filter;
|
|
15452
|
-
const
|
|
15522
|
+
const { path_prefix, exclude_path_prefix, tags, frontmatter } = filter;
|
|
15523
|
+
const includePrefixes = normalizePrefixList(path_prefix, "path_prefix", "INVALID_FILTER");
|
|
15524
|
+
const excludePrefixes = normalizePrefixList(
|
|
15525
|
+
exclude_path_prefix,
|
|
15526
|
+
"exclude_path_prefix",
|
|
15527
|
+
"INVALID_FILTER"
|
|
15528
|
+
);
|
|
15529
|
+
const hasInclude = includePrefixes !== void 0;
|
|
15530
|
+
const hasExclude = excludePrefixes !== void 0;
|
|
15453
15531
|
const hasTags = Array.isArray(tags) && tags.length > 0;
|
|
15454
15532
|
const hasFrontmatter = frontmatter !== void 0 && Object.keys(frontmatter).length > 0;
|
|
15455
|
-
if (!
|
|
15533
|
+
if (!hasInclude && !hasExclude && !hasTags && !hasFrontmatter) {
|
|
15456
15534
|
throw new ToolHandlerError(
|
|
15457
15535
|
"INVALID_FILTER",
|
|
15458
|
-
"filter must specify at least one of: path_prefix, tags, frontmatter"
|
|
15536
|
+
"filter must specify at least one of: path_prefix, exclude_path_prefix, tags, frontmatter"
|
|
15459
15537
|
);
|
|
15460
15538
|
}
|
|
15461
|
-
|
|
15462
|
-
|
|
15463
|
-
(
|
|
15464
|
-
|
|
15465
|
-
|
|
15466
|
-
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15473
|
-
|
|
15539
|
+
if (!hasTags && !hasFrontmatter) {
|
|
15540
|
+
const collected = /* @__PURE__ */ new Set();
|
|
15541
|
+
if (hasInclude) {
|
|
15542
|
+
const total = includePrefixes.length;
|
|
15543
|
+
const scans = await Promise.all(
|
|
15544
|
+
includePrefixes.map(
|
|
15545
|
+
(p, i) => deps.reader.scan({ pathPrefix: p }).catch((err) => rethrowPathNotFoundWithIndex(err, p, i, total))
|
|
15546
|
+
)
|
|
15547
|
+
);
|
|
15548
|
+
for (const list of scans) for (const p of list) collected.add(p);
|
|
15549
|
+
} else {
|
|
15550
|
+
for (const p of await deps.reader.scan({ pathPrefix: void 0 })) collected.add(p);
|
|
15551
|
+
}
|
|
15552
|
+
if (hasExclude) {
|
|
15553
|
+
for (const p of [...collected]) {
|
|
15554
|
+
if (matchesAnyPrefix(p, excludePrefixes)) collected.delete(p);
|
|
15474
15555
|
}
|
|
15475
|
-
throw err;
|
|
15476
15556
|
}
|
|
15557
|
+
return collected;
|
|
15477
15558
|
}
|
|
15478
15559
|
if (hasFrontmatter) {
|
|
15479
15560
|
validateFilter(frontmatter);
|
|
15480
15561
|
}
|
|
15481
15562
|
const internalFilter = compileFilter({ tags, frontmatter });
|
|
15482
15563
|
const effectiveFilter = applyDefaultRegexOptions(internalFilter);
|
|
15483
|
-
const
|
|
15484
|
-
|
|
15485
|
-
|
|
15486
|
-
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
15564
|
+
const scanPrefixes = hasInclude ? [...includePrefixes] : [void 0];
|
|
15565
|
+
const totalIncludes = scanPrefixes.length;
|
|
15566
|
+
const batched = await Promise.all(
|
|
15567
|
+
scanPrefixes.map((prefix, i) => {
|
|
15568
|
+
const promise2 = collectMatchingPaths(
|
|
15569
|
+
{
|
|
15570
|
+
filter: effectiveFilter,
|
|
15571
|
+
pathPrefix: prefix,
|
|
15572
|
+
includeContent: false,
|
|
15573
|
+
excludePathPrefixes: hasExclude ? excludePrefixes : void 0
|
|
15574
|
+
},
|
|
15575
|
+
deps
|
|
15576
|
+
);
|
|
15577
|
+
return prefix === void 0 ? promise2 : promise2.catch(
|
|
15578
|
+
(err) => rethrowPathNotFoundWithIndex(err, prefix, i, totalIncludes)
|
|
15579
|
+
);
|
|
15580
|
+
})
|
|
15490
15581
|
);
|
|
15491
|
-
|
|
15582
|
+
const out = /* @__PURE__ */ new Set();
|
|
15583
|
+
for (const rows of batched) {
|
|
15584
|
+
for (const row of rows) out.add(row.record.path);
|
|
15585
|
+
}
|
|
15586
|
+
return out;
|
|
15492
15587
|
};
|
|
15493
15588
|
}
|
|
15494
15589
|
function compileFilter(parts) {
|
|
@@ -16013,8 +16108,10 @@ async function executeMultiRetrieval(input) {
|
|
|
16013
16108
|
}
|
|
16014
16109
|
|
|
16015
16110
|
// src/modules/semantic/tools/search-notes.ts
|
|
16111
|
+
var prefixSchema = external_exports.union([external_exports.string(), external_exports.array(external_exports.string()).min(1)]);
|
|
16016
16112
|
var filterSchema = external_exports.object({
|
|
16017
|
-
path_prefix:
|
|
16113
|
+
path_prefix: prefixSchema.optional(),
|
|
16114
|
+
exclude_path_prefix: prefixSchema.optional(),
|
|
16018
16115
|
tags: external_exports.array(external_exports.string()).optional(),
|
|
16019
16116
|
frontmatter: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
|
|
16020
16117
|
});
|
|
@@ -16034,10 +16131,11 @@ function wrapDependencyError2(error48, message, details) {
|
|
|
16034
16131
|
return new ToolHandlerError("DEPENDENCY_ERROR", message, { details, cause: error48 });
|
|
16035
16132
|
}
|
|
16036
16133
|
function isFilterEmpty(filter) {
|
|
16037
|
-
const
|
|
16134
|
+
const hasInclude = filter.path_prefix !== void 0 && (Array.isArray(filter.path_prefix) ? filter.path_prefix.length > 0 : filter.path_prefix !== "");
|
|
16135
|
+
const hasExclude = filter.exclude_path_prefix !== void 0 && (Array.isArray(filter.exclude_path_prefix) ? filter.exclude_path_prefix.length > 0 : filter.exclude_path_prefix !== "");
|
|
16038
16136
|
const hasTags = Array.isArray(filter.tags) && filter.tags.length > 0;
|
|
16039
16137
|
const hasFm = filter.frontmatter !== void 0 && Object.keys(filter.frontmatter).length > 0;
|
|
16040
|
-
return !
|
|
16138
|
+
return !hasInclude && !hasExclude && !hasTags && !hasFm;
|
|
16041
16139
|
}
|
|
16042
16140
|
function narrowSources(sources, allowed) {
|
|
16043
16141
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -16068,7 +16166,7 @@ async function runSearchForEntry(entry, input, deps) {
|
|
|
16068
16166
|
if (isFilterEmpty(input.filter)) {
|
|
16069
16167
|
throw new ToolHandlerError(
|
|
16070
16168
|
"INVALID_ARGUMENT",
|
|
16071
|
-
"filter must specify at least one of: path_prefix, tags, frontmatter"
|
|
16169
|
+
"filter must specify at least one of: path_prefix, exclude_path_prefix, tags, frontmatter"
|
|
16072
16170
|
);
|
|
16073
16171
|
}
|
|
16074
16172
|
let allowed;
|
|
@@ -16208,13 +16306,15 @@ function buildSearchNotesTool(deps) {
|
|
|
16208
16306
|
"",
|
|
16209
16307
|
"PRE-FILTER (filter parameter):",
|
|
16210
16308
|
"- filter: optional structural narrowing applied BEFORE semantic ranking. Best when vault has many narrative notes that crowd top-K on a niche query.",
|
|
16211
|
-
" Shape: { path_prefix?, tags?, frontmatter? }. At least one field required.",
|
|
16212
|
-
' - path_prefix: scope to a folder (e.g. "
|
|
16309
|
+
" Shape: { path_prefix?, exclude_path_prefix?, tags?, frontmatter? }. At least one field required.",
|
|
16310
|
+
' - path_prefix: scope to a folder, or array of folders for OR-semantics (e.g. ["Tasks/", "Reflections/"]).',
|
|
16311
|
+
' - exclude_path_prefix: drop notes whose path starts with any of the listed prefixes (e.g. ["Resources/", "Archive/"]). Valid as the sole filter field \u2014 "search the whole vault except these subtrees".',
|
|
16213
16312
|
' - tags: notes that have ANY of these tags (OR within the array; no leading "#").',
|
|
16214
16313
|
' - frontmatter: sift filter on frontmatter keys (e.g. { type: "reflection", status: "active" }). Same operator allow-list as query_notes.',
|
|
16215
|
-
" Composition:
|
|
16314
|
+
" Composition: include \u2192 exclude \u2192 tags \u2192 frontmatter \u2192 threshold \u2192 semantic. Use this instead of querying twice and intersecting on the client.",
|
|
16216
16315
|
'- scoped recall: search_notes({query: "trading lessons", filter: {tags: ["trading"]}}) \u2014 semantic only inside notes tagged trading.',
|
|
16217
|
-
'-
|
|
16316
|
+
'- carve out noise: search_notes({query: "active thinking", filter: {exclude_path_prefix: ["Resources/", "Archive/"]}, mode: "deep"}).',
|
|
16317
|
+
'- scoped multi-query: search_notes({query: ["embeddings","\u0432\u0435\u043A\u0442\u043E\u0440\u043D\u0438\u0439 \u043F\u043E\u0448\u0443\u043A"], filter: {path_prefix: ["Resources/", "Inbox/"]}, mode: "deep"}).',
|
|
16218
16318
|
...registry2.isMulti() ? [
|
|
16219
16319
|
"",
|
|
16220
16320
|
"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: [...]` (vaults without a semantic index are listed in `skipped_vaults`).",
|
|
@@ -16708,12 +16808,14 @@ async function runQueryForEntry(entry, input) {
|
|
|
16708
16808
|
}));
|
|
16709
16809
|
return { results, count: raw.count, truncated: raw.truncated };
|
|
16710
16810
|
}
|
|
16811
|
+
var queryNotesPrefixSchema = external_exports.union([external_exports.string(), external_exports.array(external_exports.string()).min(1)]);
|
|
16711
16812
|
function buildQueryNotesTool(deps) {
|
|
16712
16813
|
const { registry: registry2 } = deps;
|
|
16713
16814
|
const inputSchema = external_exports.object({
|
|
16714
16815
|
...vaultParamShape(registry2),
|
|
16715
16816
|
filter: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
16716
|
-
path_prefix:
|
|
16817
|
+
path_prefix: queryNotesPrefixSchema.optional(),
|
|
16818
|
+
exclude_path_prefix: queryNotesPrefixSchema.optional(),
|
|
16717
16819
|
sort: queryNotesSortSchema.optional(),
|
|
16718
16820
|
limit: external_exports.number().int().min(1).max(1e3).optional(),
|
|
16719
16821
|
include_content: external_exports.boolean().optional()
|
|
@@ -16721,7 +16823,7 @@ function buildQueryNotesTool(deps) {
|
|
|
16721
16823
|
return {
|
|
16722
16824
|
name: "query_notes",
|
|
16723
16825
|
title: "Query Notes",
|
|
16724
|
-
description: 'Run a structured MongoDB-style query against the vault\'s frontmatter, tags, and wikilink graph. `filter` is a sift/MongoDB filter object evaluated against `NoteRecord` shape `{ path, frontmatter, tags, backlink_count }` \u2014 `tags` is an array of strings (no leading `#`) extracted from the `tags:` frontmatter field; `backlink_count` is the number of vault-wide wikilinks (and `![[embeds]]`) that point at the note. Reference frontmatter keys with the dotted prefix `frontmatter.<key>`. Supported operators: `$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$exists`, `$regex`, `$options`, `$and`, `$or`, `$nor`, `$not`. `$regex` is case-insensitive by default; pass `$options` (e.g. `\'\'` for case-sensitive, `\'m\'` for multiline-only) to override. Optional `path_prefix` restricts the scan to a vault subtree (vault-relative POSIX, no leading slash). Optional `sort` is `{ field, order }` \u2014 `field` must be `"path"`, `"backlink_count"`, or start with `"frontmatter."`. Optional `limit` defaults to 100, max 1000. Optional `include_content` (default false) \u2014 when true, each result also carries `content` (note body). Returns `{ results, count, truncated }`; each result item carries `vault` and `backlink_count`. `truncated` is true when more notes matched than `limit` allowed. Reads directly from disk and does not require Obsidian to be running.' + describeMultiVault(
|
|
16826
|
+
description: 'Run a structured MongoDB-style query against the vault\'s frontmatter, tags, and wikilink graph. `filter` is a sift/MongoDB filter object evaluated against `NoteRecord` shape `{ path, frontmatter, tags, backlink_count }` \u2014 `tags` is an array of strings (no leading `#`) extracted from the `tags:` frontmatter field; `backlink_count` is the number of vault-wide wikilinks (and `![[embeds]]`) that point at the note. Reference frontmatter keys with the dotted prefix `frontmatter.<key>`. Supported operators: `$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$exists`, `$regex`, `$options`, `$and`, `$or`, `$nor`, `$not`. `$regex` is case-insensitive by default; pass `$options` (e.g. `\'\'` for case-sensitive, `\'m\'` for multiline-only) to override. Optional `path_prefix` restricts the scan to a vault subtree (vault-relative POSIX, no leading slash); pass an array for OR-semantics across multiple subtrees (e.g. `["Tasks/", "Reflections/"]`). Optional `exclude_path_prefix` (string or array) drops notes whose path starts with any listed prefix \u2014 valid as the sole path filter, e.g. `["Resources/", "Archive/"]` to search the whole vault except those subtrees. Optional `sort` is `{ field, order }` \u2014 `field` must be `"path"`, `"backlink_count"`, or start with `"frontmatter."`. Optional `limit` defaults to 100, max 1000. Optional `include_content` (default false) \u2014 when true, each result also carries `content` (note body). Returns `{ results, count, truncated }`; each result item carries `vault` and `backlink_count`. `truncated` is true when more notes matched than `limit` allowed. Reads directly from disk and does not require Obsidian to be running.' + describeMultiVault(
|
|
16725
16827
|
registry2,
|
|
16726
16828
|
'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.'
|
|
16727
16829
|
),
|