neuro-vault-mcp 7.0.0 → 8.1.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 +3 -3
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +204 -61
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 🧠💾 **Make your personal vault usable by agents.** Low-token retrieval, explicit provenance, and safe writes for your Obsidian notes — in Claude Code, Cursor, Windsurf, and any MCP client.
|
|
4
4
|
|
|
5
|
-
https://github.com/user-attachments/assets/25c1bafb-7b90-43ac-aa50-50e85705fb5b
|
|
5
|
+
[https://github.com/user-attachments/assets/25c1bafb-7b90-43ac-aa50-50e85705fb5b](https://github.com/user-attachments/assets/5ac86cd7-2d67-420d-b940-dba4ae1a2ccb)
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/neuro-vault-mcp)
|
|
8
8
|
[](https://nodejs.org)
|
|
@@ -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
|
|
|
@@ -140,7 +140,7 @@ Two vaults cannot share the same directory basename — the basename doubles as
|
|
|
140
140
|
With multiple vaults registered:
|
|
141
141
|
|
|
142
142
|
- **Every tool** accepts an optional `vault: "<name>"` parameter to target a specific vault.
|
|
143
|
-
- **`search_notes`, `query_notes`, and `
|
|
143
|
+
- **`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.
|
|
144
144
|
- **All other tools** (writes, reads of specific paths, single-vault diagnostics) require an explicit `vault` in multi-vault mode. Omitting it returns `VAULT_REQUIRED`.
|
|
145
145
|
- **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`.
|
|
146
146
|
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -14228,6 +14228,16 @@ var ToolHandlerError = class extends Error {
|
|
|
14228
14228
|
}
|
|
14229
14229
|
}
|
|
14230
14230
|
};
|
|
14231
|
+
var FATAL_TOOL_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
14232
|
+
"INVALID_ARGUMENT",
|
|
14233
|
+
"INVALID_PARAMS",
|
|
14234
|
+
"INVALID_FILTER",
|
|
14235
|
+
"VAULT_REQUIRED",
|
|
14236
|
+
"VAULT_NOT_FOUND"
|
|
14237
|
+
]);
|
|
14238
|
+
function isFatalToolError(error48) {
|
|
14239
|
+
return error48 instanceof ToolHandlerError && FATAL_TOOL_ERROR_CODES.has(error48.code);
|
|
14240
|
+
}
|
|
14231
14241
|
function isPlainRecord(value) {
|
|
14232
14242
|
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
14233
14243
|
}
|
|
@@ -15192,6 +15202,38 @@ function normalizeVaultPathPrefix(raw, invalid) {
|
|
|
15192
15202
|
return slashed.replace(/\/+$/, "");
|
|
15193
15203
|
}
|
|
15194
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
|
+
|
|
15195
15237
|
// src/lib/obsidian/query/whitelist.ts
|
|
15196
15238
|
var ALLOWED_OPERATORS = /* @__PURE__ */ new Set([
|
|
15197
15239
|
"$eq",
|
|
@@ -15279,6 +15321,9 @@ async function collectMatchingPaths(input, deps) {
|
|
|
15279
15321
|
}
|
|
15280
15322
|
continue;
|
|
15281
15323
|
}
|
|
15324
|
+
if (input.excludePathPrefixes !== void 0 && matchesAnyPrefix(item.path, input.excludePathPrefixes)) {
|
|
15325
|
+
continue;
|
|
15326
|
+
}
|
|
15282
15327
|
const backlinkCount = graph?.getBacklinkCount(item.path) ?? 0;
|
|
15283
15328
|
const record2 = toNoteRecord(item, backlinkCount);
|
|
15284
15329
|
let isMatch;
|
|
@@ -15302,16 +15347,33 @@ async function runQueryNotes(input, reader, graph) {
|
|
|
15302
15347
|
if (graph) {
|
|
15303
15348
|
await graph.ensureFresh();
|
|
15304
15349
|
}
|
|
15305
|
-
const
|
|
15306
|
-
const
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
|
|
15350
|
+
const sortOrderMatchesScan = !validated.sort || validated.sort.field === "path" && validated.sort.order === "asc";
|
|
15351
|
+
const includes = validated.includePrefixes ?? [void 0];
|
|
15352
|
+
const multiPrefix = includes.length > 1;
|
|
15353
|
+
const earlyExitAfter = sortOrderMatchesScan && !multiPrefix ? validated.limit : void 0;
|
|
15354
|
+
const batches = await Promise.all(
|
|
15355
|
+
includes.map(
|
|
15356
|
+
(prefix) => collectMatchingPaths(
|
|
15357
|
+
{
|
|
15358
|
+
filter: effectiveFilter,
|
|
15359
|
+
pathPrefix: prefix,
|
|
15360
|
+
includeContent: validated.includeContent,
|
|
15361
|
+
earlyExitAfter,
|
|
15362
|
+
excludePathPrefixes: validated.excludePrefixes
|
|
15363
|
+
},
|
|
15364
|
+
{ reader, graph }
|
|
15365
|
+
)
|
|
15366
|
+
)
|
|
15314
15367
|
);
|
|
15368
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15369
|
+
const matched = [];
|
|
15370
|
+
for (const rows of batches) {
|
|
15371
|
+
for (const row of rows) {
|
|
15372
|
+
if (seen.has(row.record.path)) continue;
|
|
15373
|
+
seen.add(row.record.path);
|
|
15374
|
+
matched.push(row);
|
|
15375
|
+
}
|
|
15376
|
+
}
|
|
15315
15377
|
if (validated.sort) {
|
|
15316
15378
|
sortInPlace(matched, validated.sort);
|
|
15317
15379
|
}
|
|
@@ -15341,13 +15403,12 @@ function validateInput(input) {
|
|
|
15341
15403
|
if (typeof input.filter !== "object" || Array.isArray(input.filter)) {
|
|
15342
15404
|
throw invalidParams("filter must be a JSON object", "filter");
|
|
15343
15405
|
}
|
|
15344
|
-
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15349
|
-
|
|
15350
|
-
}
|
|
15406
|
+
const includePrefixes = normalizePrefixList(input.path_prefix, "path_prefix", "INVALID_PARAMS");
|
|
15407
|
+
const excludePrefixes = normalizePrefixList(
|
|
15408
|
+
input.exclude_path_prefix,
|
|
15409
|
+
"exclude_path_prefix",
|
|
15410
|
+
"INVALID_PARAMS"
|
|
15411
|
+
);
|
|
15351
15412
|
let sort;
|
|
15352
15413
|
if (input.sort !== void 0) {
|
|
15353
15414
|
sort = validateSort(input.sort);
|
|
@@ -15368,15 +15429,13 @@ function validateInput(input) {
|
|
|
15368
15429
|
}
|
|
15369
15430
|
return {
|
|
15370
15431
|
filter: input.filter,
|
|
15371
|
-
|
|
15432
|
+
includePrefixes,
|
|
15433
|
+
excludePrefixes,
|
|
15372
15434
|
sort,
|
|
15373
15435
|
limit,
|
|
15374
15436
|
includeContent
|
|
15375
15437
|
};
|
|
15376
15438
|
}
|
|
15377
|
-
function normalizeScanPrefixInput(raw) {
|
|
15378
|
-
return normalizeVaultPathPrefix(raw, (message) => invalidParams(message, "path_prefix"));
|
|
15379
|
-
}
|
|
15380
15439
|
function validateSort(raw) {
|
|
15381
15440
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
15382
15441
|
throw invalidParams("sort must be an object { field, order }", "sort");
|
|
@@ -15438,47 +15497,71 @@ function invalidParams(message, field) {
|
|
|
15438
15497
|
// src/lib/obsidian/query/list-matching-paths.ts
|
|
15439
15498
|
function createListMatchingPaths(deps) {
|
|
15440
15499
|
return async (filter) => {
|
|
15441
|
-
const { path_prefix, tags, frontmatter } = filter;
|
|
15442
|
-
const
|
|
15500
|
+
const { path_prefix, exclude_path_prefix, tags, frontmatter } = filter;
|
|
15501
|
+
const includePrefixes = normalizePrefixList(path_prefix, "path_prefix", "INVALID_FILTER");
|
|
15502
|
+
const excludePrefixes = normalizePrefixList(
|
|
15503
|
+
exclude_path_prefix,
|
|
15504
|
+
"exclude_path_prefix",
|
|
15505
|
+
"INVALID_FILTER"
|
|
15506
|
+
);
|
|
15507
|
+
const hasInclude = includePrefixes !== void 0;
|
|
15508
|
+
const hasExclude = excludePrefixes !== void 0;
|
|
15443
15509
|
const hasTags = Array.isArray(tags) && tags.length > 0;
|
|
15444
15510
|
const hasFrontmatter = frontmatter !== void 0 && Object.keys(frontmatter).length > 0;
|
|
15445
|
-
if (!
|
|
15511
|
+
if (!hasInclude && !hasExclude && !hasTags && !hasFrontmatter) {
|
|
15446
15512
|
throw new ToolHandlerError(
|
|
15447
15513
|
"INVALID_FILTER",
|
|
15448
|
-
"filter must specify at least one of: path_prefix, tags, frontmatter"
|
|
15514
|
+
"filter must specify at least one of: path_prefix, exclude_path_prefix, tags, frontmatter"
|
|
15449
15515
|
);
|
|
15450
15516
|
}
|
|
15451
|
-
|
|
15452
|
-
|
|
15453
|
-
(message) => new ToolHandlerError("INVALID_FILTER", message)
|
|
15454
|
-
) : void 0;
|
|
15455
|
-
if (hasPathPrefix && !hasTags && !hasFrontmatter) {
|
|
15517
|
+
if (!hasTags && !hasFrontmatter) {
|
|
15518
|
+
const collected = /* @__PURE__ */ new Set();
|
|
15456
15519
|
try {
|
|
15457
|
-
|
|
15458
|
-
|
|
15520
|
+
if (hasInclude) {
|
|
15521
|
+
const scans = await Promise.all(
|
|
15522
|
+
includePrefixes.map((p) => deps.reader.scan({ pathPrefix: p }))
|
|
15523
|
+
);
|
|
15524
|
+
for (const list of scans) for (const p of list) collected.add(p);
|
|
15525
|
+
} else {
|
|
15526
|
+
for (const p of await deps.reader.scan({ pathPrefix: void 0 })) collected.add(p);
|
|
15527
|
+
}
|
|
15459
15528
|
} catch (err) {
|
|
15460
15529
|
if (err instanceof ScanPathNotFoundError) {
|
|
15461
|
-
throw new ToolHandlerError("PATH_NOT_FOUND", err.message
|
|
15462
|
-
details: { path_prefix: normalizedPrefix }
|
|
15463
|
-
});
|
|
15530
|
+
throw new ToolHandlerError("PATH_NOT_FOUND", err.message);
|
|
15464
15531
|
}
|
|
15465
15532
|
throw err;
|
|
15466
15533
|
}
|
|
15534
|
+
if (hasExclude) {
|
|
15535
|
+
for (const p of [...collected]) {
|
|
15536
|
+
if (matchesAnyPrefix(p, excludePrefixes)) collected.delete(p);
|
|
15537
|
+
}
|
|
15538
|
+
}
|
|
15539
|
+
return collected;
|
|
15467
15540
|
}
|
|
15468
15541
|
if (hasFrontmatter) {
|
|
15469
15542
|
validateFilter(frontmatter);
|
|
15470
15543
|
}
|
|
15471
15544
|
const internalFilter = compileFilter({ tags, frontmatter });
|
|
15472
15545
|
const effectiveFilter = applyDefaultRegexOptions(internalFilter);
|
|
15473
|
-
const
|
|
15474
|
-
|
|
15475
|
-
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15546
|
+
const scanPrefixes = hasInclude ? [...includePrefixes] : [void 0];
|
|
15547
|
+
const batched = await Promise.all(
|
|
15548
|
+
scanPrefixes.map(
|
|
15549
|
+
(prefix) => collectMatchingPaths(
|
|
15550
|
+
{
|
|
15551
|
+
filter: effectiveFilter,
|
|
15552
|
+
pathPrefix: prefix,
|
|
15553
|
+
includeContent: false,
|
|
15554
|
+
excludePathPrefixes: hasExclude ? excludePrefixes : void 0
|
|
15555
|
+
},
|
|
15556
|
+
deps
|
|
15557
|
+
)
|
|
15558
|
+
)
|
|
15480
15559
|
);
|
|
15481
|
-
|
|
15560
|
+
const out = /* @__PURE__ */ new Set();
|
|
15561
|
+
for (const rows of batched) {
|
|
15562
|
+
for (const row of rows) out.add(row.record.path);
|
|
15563
|
+
}
|
|
15564
|
+
return out;
|
|
15482
15565
|
};
|
|
15483
15566
|
}
|
|
15484
15567
|
function compileFilter(parts) {
|
|
@@ -15715,20 +15798,73 @@ function buildGetStatsTool(deps) {
|
|
|
15715
15798
|
}
|
|
15716
15799
|
|
|
15717
15800
|
// src/lib/fan-out.ts
|
|
15801
|
+
function findFatalRejection(settled) {
|
|
15802
|
+
for (const outcome of settled) {
|
|
15803
|
+
if (outcome.status === "rejected" && isFatalToolError(outcome.reason)) {
|
|
15804
|
+
return outcome.reason;
|
|
15805
|
+
}
|
|
15806
|
+
}
|
|
15807
|
+
return void 0;
|
|
15808
|
+
}
|
|
15809
|
+
function mapRejectionToFailedVault(vault, reason) {
|
|
15810
|
+
if (reason instanceof ToolHandlerError) {
|
|
15811
|
+
return {
|
|
15812
|
+
vault,
|
|
15813
|
+
error: {
|
|
15814
|
+
code: reason.code,
|
|
15815
|
+
message: reason.message,
|
|
15816
|
+
...reason.details ? { details: reason.details } : {}
|
|
15817
|
+
}
|
|
15818
|
+
};
|
|
15819
|
+
}
|
|
15820
|
+
return {
|
|
15821
|
+
vault,
|
|
15822
|
+
error: {
|
|
15823
|
+
code: "INTERNAL_ERROR",
|
|
15824
|
+
message: reason instanceof Error ? reason.message : String(reason)
|
|
15825
|
+
}
|
|
15826
|
+
};
|
|
15827
|
+
}
|
|
15718
15828
|
async function runFanOut(registry2, fn) {
|
|
15719
15829
|
const entries = registry2.list();
|
|
15720
|
-
const
|
|
15721
|
-
|
|
15722
|
-
)
|
|
15723
|
-
|
|
15830
|
+
const settled = await Promise.allSettled(entries.map((entry) => fn(entry)));
|
|
15831
|
+
const fatalError = findFatalRejection(settled);
|
|
15832
|
+
if (fatalError) {
|
|
15833
|
+
throw fatalError;
|
|
15834
|
+
}
|
|
15835
|
+
const results = [];
|
|
15836
|
+
const failed = [];
|
|
15837
|
+
for (let i = 0; i < settled.length; i++) {
|
|
15838
|
+
const outcome = settled[i];
|
|
15839
|
+
const vault = entries[i].name;
|
|
15840
|
+
if (outcome.status === "fulfilled") {
|
|
15841
|
+
results.push({ vault, ...outcome.value });
|
|
15842
|
+
} else {
|
|
15843
|
+
failed.push(mapRejectionToFailedVault(vault, outcome.reason));
|
|
15844
|
+
}
|
|
15845
|
+
}
|
|
15846
|
+
return { results_by_vault: results, skipped_vaults: [], failed_vaults: failed };
|
|
15724
15847
|
}
|
|
15725
15848
|
async function runSemanticFanOut(registry2, fn) {
|
|
15726
15849
|
const eligible = registry2.semanticAvailableEntries();
|
|
15727
15850
|
const skipped = registry2.list().filter((e) => !e.semanticAvailable).map((e) => ({ vault: e.name, reason: "SEMANTIC_INDEX_NOT_FOUND" }));
|
|
15728
|
-
const
|
|
15729
|
-
|
|
15730
|
-
)
|
|
15731
|
-
|
|
15851
|
+
const settled = await Promise.allSettled(eligible.map((entry) => fn(entry)));
|
|
15852
|
+
const fatalError = findFatalRejection(settled);
|
|
15853
|
+
if (fatalError) {
|
|
15854
|
+
throw fatalError;
|
|
15855
|
+
}
|
|
15856
|
+
const results = [];
|
|
15857
|
+
const failed = [];
|
|
15858
|
+
for (let i = 0; i < settled.length; i++) {
|
|
15859
|
+
const outcome = settled[i];
|
|
15860
|
+
const vault = eligible[i].name;
|
|
15861
|
+
if (outcome.status === "fulfilled") {
|
|
15862
|
+
results.push({ vault, ...outcome.value });
|
|
15863
|
+
} else {
|
|
15864
|
+
failed.push(mapRejectionToFailedVault(vault, outcome.reason));
|
|
15865
|
+
}
|
|
15866
|
+
}
|
|
15867
|
+
return { results_by_vault: results, skipped_vaults: skipped, failed_vaults: failed };
|
|
15732
15868
|
}
|
|
15733
15869
|
|
|
15734
15870
|
// src/modules/semantic/retrieval-policy.ts
|
|
@@ -15950,8 +16086,10 @@ async function executeMultiRetrieval(input) {
|
|
|
15950
16086
|
}
|
|
15951
16087
|
|
|
15952
16088
|
// src/modules/semantic/tools/search-notes.ts
|
|
16089
|
+
var prefixSchema = external_exports.union([external_exports.string(), external_exports.array(external_exports.string()).min(1)]);
|
|
15953
16090
|
var filterSchema = external_exports.object({
|
|
15954
|
-
path_prefix:
|
|
16091
|
+
path_prefix: prefixSchema.optional(),
|
|
16092
|
+
exclude_path_prefix: prefixSchema.optional(),
|
|
15955
16093
|
tags: external_exports.array(external_exports.string()).optional(),
|
|
15956
16094
|
frontmatter: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
|
|
15957
16095
|
});
|
|
@@ -15971,10 +16109,11 @@ function wrapDependencyError2(error48, message, details) {
|
|
|
15971
16109
|
return new ToolHandlerError("DEPENDENCY_ERROR", message, { details, cause: error48 });
|
|
15972
16110
|
}
|
|
15973
16111
|
function isFilterEmpty(filter) {
|
|
15974
|
-
const
|
|
16112
|
+
const hasInclude = filter.path_prefix !== void 0 && (Array.isArray(filter.path_prefix) ? filter.path_prefix.length > 0 : filter.path_prefix !== "");
|
|
16113
|
+
const hasExclude = filter.exclude_path_prefix !== void 0 && (Array.isArray(filter.exclude_path_prefix) ? filter.exclude_path_prefix.length > 0 : filter.exclude_path_prefix !== "");
|
|
15975
16114
|
const hasTags = Array.isArray(filter.tags) && filter.tags.length > 0;
|
|
15976
16115
|
const hasFm = filter.frontmatter !== void 0 && Object.keys(filter.frontmatter).length > 0;
|
|
15977
|
-
return !
|
|
16116
|
+
return !hasInclude && !hasExclude && !hasTags && !hasFm;
|
|
15978
16117
|
}
|
|
15979
16118
|
function narrowSources(sources, allowed) {
|
|
15980
16119
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -16005,7 +16144,7 @@ async function runSearchForEntry(entry, input, deps) {
|
|
|
16005
16144
|
if (isFilterEmpty(input.filter)) {
|
|
16006
16145
|
throw new ToolHandlerError(
|
|
16007
16146
|
"INVALID_ARGUMENT",
|
|
16008
|
-
"filter must specify at least one of: path_prefix, tags, frontmatter"
|
|
16147
|
+
"filter must specify at least one of: path_prefix, exclude_path_prefix, tags, frontmatter"
|
|
16009
16148
|
);
|
|
16010
16149
|
}
|
|
16011
16150
|
let allowed;
|
|
@@ -16145,13 +16284,15 @@ function buildSearchNotesTool(deps) {
|
|
|
16145
16284
|
"",
|
|
16146
16285
|
"PRE-FILTER (filter parameter):",
|
|
16147
16286
|
"- filter: optional structural narrowing applied BEFORE semantic ranking. Best when vault has many narrative notes that crowd top-K on a niche query.",
|
|
16148
|
-
" Shape: { path_prefix?, tags?, frontmatter? }. At least one field required.",
|
|
16149
|
-
' - path_prefix: scope to a folder (e.g. "
|
|
16287
|
+
" Shape: { path_prefix?, exclude_path_prefix?, tags?, frontmatter? }. At least one field required.",
|
|
16288
|
+
' - path_prefix: scope to a folder, or array of folders for OR-semantics (e.g. ["Tasks/", "Reflections/"]).',
|
|
16289
|
+
' - 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".',
|
|
16150
16290
|
' - tags: notes that have ANY of these tags (OR within the array; no leading "#").',
|
|
16151
16291
|
' - frontmatter: sift filter on frontmatter keys (e.g. { type: "reflection", status: "active" }). Same operator allow-list as query_notes.',
|
|
16152
|
-
" Composition:
|
|
16292
|
+
" Composition: include \u2192 exclude \u2192 tags \u2192 frontmatter \u2192 threshold \u2192 semantic. Use this instead of querying twice and intersecting on the client.",
|
|
16153
16293
|
'- scoped recall: search_notes({query: "trading lessons", filter: {tags: ["trading"]}}) \u2014 semantic only inside notes tagged trading.',
|
|
16154
|
-
'-
|
|
16294
|
+
'- carve out noise: search_notes({query: "active thinking", filter: {exclude_path_prefix: ["Resources/", "Archive/"]}, mode: "deep"}).',
|
|
16295
|
+
'- 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"}).',
|
|
16155
16296
|
...registry2.isMulti() ? [
|
|
16156
16297
|
"",
|
|
16157
16298
|
"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`).",
|
|
@@ -16645,12 +16786,14 @@ async function runQueryForEntry(entry, input) {
|
|
|
16645
16786
|
}));
|
|
16646
16787
|
return { results, count: raw.count, truncated: raw.truncated };
|
|
16647
16788
|
}
|
|
16789
|
+
var queryNotesPrefixSchema = external_exports.union([external_exports.string(), external_exports.array(external_exports.string()).min(1)]);
|
|
16648
16790
|
function buildQueryNotesTool(deps) {
|
|
16649
16791
|
const { registry: registry2 } = deps;
|
|
16650
16792
|
const inputSchema = external_exports.object({
|
|
16651
16793
|
...vaultParamShape(registry2),
|
|
16652
16794
|
filter: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
16653
|
-
path_prefix:
|
|
16795
|
+
path_prefix: queryNotesPrefixSchema.optional(),
|
|
16796
|
+
exclude_path_prefix: queryNotesPrefixSchema.optional(),
|
|
16654
16797
|
sort: queryNotesSortSchema.optional(),
|
|
16655
16798
|
limit: external_exports.number().int().min(1).max(1e3).optional(),
|
|
16656
16799
|
include_content: external_exports.boolean().optional()
|
|
@@ -16658,7 +16801,7 @@ function buildQueryNotesTool(deps) {
|
|
|
16658
16801
|
return {
|
|
16659
16802
|
name: "query_notes",
|
|
16660
16803
|
title: "Query Notes",
|
|
16661
|
-
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(
|
|
16804
|
+
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(
|
|
16662
16805
|
registry2,
|
|
16663
16806
|
'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.'
|
|
16664
16807
|
),
|
|
@@ -17473,7 +17616,7 @@ async function buildServerInstructions(registry2) {
|
|
|
17473
17616
|
|
|
17474
17617
|
## Multi-vault mode
|
|
17475
17618
|
|
|
17476
|
-
This server is registered with multiple vaults: ${names}. Every tool accepts an optional \`vault\` parameter. For broad recall, \`search_notes\`, \`query_notes\`, and \`
|
|
17619
|
+
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\`.`;
|
|
17477
17620
|
}
|
|
17478
17621
|
for (const entry of registry2.list()) {
|
|
17479
17622
|
const extra = await readExternalAgentInstructions(entry.path);
|