neuro-vault-mcp 5.0.0 → 5.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 +12 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +202 -37
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,18 @@ One question, one answer. Your assistant stops being a file browser and starts b
|
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
|
+
### 🔍 Hybrid search: scope semantic with structural filters
|
|
66
|
+
|
|
67
|
+
`search_notes` accepts an optional `filter` to narrow the candidate set **before** semantic ranking — combining the precision of `query_notes` with the recall of vector search. Useful when domain-relevant notes are crowded out by larger narrative clusters.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{ "query": "trading lessons", "filter": { "tags": ["trading"] } }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`filter` accepts `path_prefix`, `tags` (ANY-of), and a `frontmatter` sift filter. Composition is `filter` AND `threshold` AND semantic. See the [Semantic Search guide](./docs/guide/semantic-search.md#pre-filter-filter-parameter) for full details.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
65
77
|
## 🏗 How it works
|
|
66
78
|
|
|
67
79
|
```mermaid
|
package/dist/cli.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ declare class WikilinkGraphIndex {
|
|
|
75
75
|
private rebuild;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
interface NoteFilter {
|
|
79
|
+
path_prefix?: string;
|
|
80
|
+
tags?: string[];
|
|
81
|
+
frontmatter?: Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
type ListMatchingPaths = (filter: NoteFilter) => Promise<Set<string>>;
|
|
84
|
+
|
|
78
85
|
interface BlockSearchResult {
|
|
79
86
|
path: string;
|
|
80
87
|
heading: string;
|
|
@@ -126,6 +133,7 @@ interface SemanticModuleDeps {
|
|
|
126
133
|
vaultReaderFactory?: (opts: {
|
|
127
134
|
vaultRoot: string;
|
|
128
135
|
}) => VaultReader;
|
|
136
|
+
listMatchingPaths?: ListMatchingPaths;
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
type NoteIdentifier = {
|
package/dist/cli.js
CHANGED
|
@@ -1022,6 +1022,21 @@ function isPlainObject(value) {
|
|
|
1022
1022
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
+
// src/lib/obsidian/query/path-prefix.ts
|
|
1026
|
+
var WINDOWS_ABSOLUTE_PATH_RE2 = /^[A-Za-z]:[\\/]/;
|
|
1027
|
+
function normalizeVaultPathPrefix(raw, invalid) {
|
|
1028
|
+
const trimmed = raw.trim();
|
|
1029
|
+
if (trimmed === "" || trimmed === "." || trimmed === "./") return void 0;
|
|
1030
|
+
if (trimmed.startsWith("/") || WINDOWS_ABSOLUTE_PATH_RE2.test(trimmed)) {
|
|
1031
|
+
throw invalid("path_prefix must be vault-relative");
|
|
1032
|
+
}
|
|
1033
|
+
const slashed = trimmed.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1034
|
+
if (slashed.split("/").some((segment) => segment === "..")) {
|
|
1035
|
+
throw invalid("path_prefix must be vault-relative");
|
|
1036
|
+
}
|
|
1037
|
+
return slashed.replace(/\/+$/, "");
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1025
1040
|
// src/lib/obsidian/query/whitelist.ts
|
|
1026
1041
|
var ALLOWED_OPERATORS = /* @__PURE__ */ new Set([
|
|
1027
1042
|
"$eq",
|
|
@@ -1071,38 +1086,32 @@ function invalidFilter(message) {
|
|
|
1071
1086
|
var sift = typeof siftModule === "function" ? siftModule : siftModule.default;
|
|
1072
1087
|
var DEFAULT_LIMIT = 100;
|
|
1073
1088
|
var HARD_LIMIT_CAP = 1e3;
|
|
1074
|
-
var WINDOWS_ABSOLUTE_PATH_RE2 = /^[A-Za-z]:[\\/]/;
|
|
1075
1089
|
var READ_BATCH_SIZE2 = 32;
|
|
1076
|
-
async function
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
const effectiveFilter = applyDefaultRegexOptions(validated.filter);
|
|
1080
|
-
if (graph) {
|
|
1081
|
-
await graph.ensureFresh();
|
|
1082
|
-
}
|
|
1090
|
+
async function collectMatchingPaths(input, deps) {
|
|
1091
|
+
const { filter, pathPrefix, includeContent, earlyExitAfter } = input;
|
|
1092
|
+
const { reader, graph } = deps;
|
|
1083
1093
|
let paths;
|
|
1084
1094
|
try {
|
|
1085
|
-
paths = await reader.scan({ pathPrefix
|
|
1095
|
+
paths = await reader.scan({ pathPrefix });
|
|
1086
1096
|
} catch (err) {
|
|
1087
1097
|
if (err instanceof ScanPathNotFoundError) {
|
|
1088
1098
|
throw new ToolHandlerError("PATH_NOT_FOUND", err.message, {
|
|
1089
|
-
details: { path_prefix:
|
|
1099
|
+
details: { path_prefix: pathPrefix }
|
|
1090
1100
|
});
|
|
1091
1101
|
}
|
|
1092
1102
|
throw err;
|
|
1093
1103
|
}
|
|
1094
1104
|
if (paths.length === 0) {
|
|
1095
|
-
return
|
|
1105
|
+
return [];
|
|
1096
1106
|
}
|
|
1097
1107
|
let matcher;
|
|
1098
1108
|
try {
|
|
1099
|
-
matcher = sift(
|
|
1109
|
+
matcher = sift(filter);
|
|
1100
1110
|
} catch (err) {
|
|
1101
1111
|
const message = err instanceof Error ? err.message : String(err);
|
|
1102
1112
|
throw new ToolHandlerError("INVALID_FILTER", `filter rejected by sift: ${message}`);
|
|
1103
1113
|
}
|
|
1104
|
-
const fields =
|
|
1105
|
-
const earlyExitAllowed = !validated.sort || validated.sort.field === "path" && validated.sort.order === "asc";
|
|
1114
|
+
const fields = includeContent ? ["frontmatter", "content"] : ["frontmatter"];
|
|
1106
1115
|
const matched = [];
|
|
1107
1116
|
for (let i = 0; i < paths.length; i += READ_BATCH_SIZE2) {
|
|
1108
1117
|
const slice = paths.slice(i, i + READ_BATCH_SIZE2);
|
|
@@ -1125,10 +1134,29 @@ async function runQueryNotes(input, reader, graph) {
|
|
|
1125
1134
|
throw new ToolHandlerError("INVALID_FILTER", `filter execution failed: ${message}`);
|
|
1126
1135
|
}
|
|
1127
1136
|
if (!isMatch) continue;
|
|
1128
|
-
matched.push(
|
|
1137
|
+
matched.push(includeContent ? { record: record2, content: item.content } : { record: record2 });
|
|
1129
1138
|
}
|
|
1130
|
-
if (
|
|
1139
|
+
if (earlyExitAfter !== void 0 && matched.length > earlyExitAfter) break;
|
|
1140
|
+
}
|
|
1141
|
+
return matched;
|
|
1142
|
+
}
|
|
1143
|
+
async function runQueryNotes(input, reader, graph) {
|
|
1144
|
+
const validated = validateInput(input);
|
|
1145
|
+
validateFilter(validated.filter);
|
|
1146
|
+
const effectiveFilter = applyDefaultRegexOptions(validated.filter);
|
|
1147
|
+
if (graph) {
|
|
1148
|
+
await graph.ensureFresh();
|
|
1131
1149
|
}
|
|
1150
|
+
const earlyExitAllowed = !validated.sort || validated.sort.field === "path" && validated.sort.order === "asc";
|
|
1151
|
+
const matched = await collectMatchingPaths(
|
|
1152
|
+
{
|
|
1153
|
+
filter: effectiveFilter,
|
|
1154
|
+
pathPrefix: validated.pathPrefix,
|
|
1155
|
+
includeContent: validated.includeContent,
|
|
1156
|
+
earlyExitAfter: earlyExitAllowed ? validated.limit : void 0
|
|
1157
|
+
},
|
|
1158
|
+
{ reader, graph }
|
|
1159
|
+
);
|
|
1132
1160
|
if (validated.sort) {
|
|
1133
1161
|
sortInPlace(matched, validated.sort);
|
|
1134
1162
|
}
|
|
@@ -1192,16 +1220,7 @@ function validateInput(input) {
|
|
|
1192
1220
|
};
|
|
1193
1221
|
}
|
|
1194
1222
|
function normalizeScanPrefixInput(raw) {
|
|
1195
|
-
|
|
1196
|
-
if (trimmed === "" || trimmed === "." || trimmed === "./") return void 0;
|
|
1197
|
-
if (trimmed.startsWith("/") || WINDOWS_ABSOLUTE_PATH_RE2.test(trimmed)) {
|
|
1198
|
-
throw invalidParams("path_prefix must be vault-relative", "path_prefix");
|
|
1199
|
-
}
|
|
1200
|
-
const slashed = trimmed.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1201
|
-
if (slashed.split("/").some((segment) => segment === "..")) {
|
|
1202
|
-
throw invalidParams("path_prefix must be vault-relative", "path_prefix");
|
|
1203
|
-
}
|
|
1204
|
-
return slashed.replace(/\/+$/, "");
|
|
1223
|
+
return normalizeVaultPathPrefix(raw, (message) => invalidParams(message, "path_prefix"));
|
|
1205
1224
|
}
|
|
1206
1225
|
function validateSort(raw) {
|
|
1207
1226
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -1261,6 +1280,66 @@ function invalidParams(message, field) {
|
|
|
1261
1280
|
return new ToolHandlerError("INVALID_PARAMS", message, { details: { field } });
|
|
1262
1281
|
}
|
|
1263
1282
|
|
|
1283
|
+
// src/lib/obsidian/query/list-matching-paths.ts
|
|
1284
|
+
function createListMatchingPaths(deps) {
|
|
1285
|
+
return async (filter) => {
|
|
1286
|
+
const { path_prefix, tags, frontmatter } = filter;
|
|
1287
|
+
const hasPathPrefix = path_prefix !== void 0 && path_prefix !== "";
|
|
1288
|
+
const hasTags = Array.isArray(tags) && tags.length > 0;
|
|
1289
|
+
const hasFrontmatter = frontmatter !== void 0 && Object.keys(frontmatter).length > 0;
|
|
1290
|
+
if (!hasPathPrefix && !hasTags && !hasFrontmatter) {
|
|
1291
|
+
throw new ToolHandlerError(
|
|
1292
|
+
"INVALID_FILTER",
|
|
1293
|
+
"filter must specify at least one of: path_prefix, tags, frontmatter"
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
const normalizedPrefix = hasPathPrefix ? normalizeVaultPathPrefix(
|
|
1297
|
+
path_prefix,
|
|
1298
|
+
(message) => new ToolHandlerError("INVALID_FILTER", message)
|
|
1299
|
+
) : void 0;
|
|
1300
|
+
if (hasPathPrefix && !hasTags && !hasFrontmatter) {
|
|
1301
|
+
try {
|
|
1302
|
+
const paths = await deps.reader.scan({ pathPrefix: normalizedPrefix });
|
|
1303
|
+
return new Set(paths);
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
if (err instanceof ScanPathNotFoundError) {
|
|
1306
|
+
throw new ToolHandlerError("PATH_NOT_FOUND", err.message, {
|
|
1307
|
+
details: { path_prefix: normalizedPrefix }
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
throw err;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (hasFrontmatter) {
|
|
1314
|
+
validateFilter(frontmatter);
|
|
1315
|
+
}
|
|
1316
|
+
const internalFilter = compileFilter({ tags, frontmatter });
|
|
1317
|
+
const effectiveFilter = applyDefaultRegexOptions(internalFilter);
|
|
1318
|
+
const rows = await collectMatchingPaths(
|
|
1319
|
+
{
|
|
1320
|
+
filter: effectiveFilter,
|
|
1321
|
+
pathPrefix: normalizedPrefix,
|
|
1322
|
+
includeContent: false
|
|
1323
|
+
},
|
|
1324
|
+
deps
|
|
1325
|
+
);
|
|
1326
|
+
return new Set(rows.map((row) => row.record.path));
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function compileFilter(parts) {
|
|
1330
|
+
const clauses = [];
|
|
1331
|
+
if (parts.tags && parts.tags.length > 0) {
|
|
1332
|
+
clauses.push({ tags: { $in: parts.tags } });
|
|
1333
|
+
}
|
|
1334
|
+
if (parts.frontmatter && Object.keys(parts.frontmatter).length > 0) {
|
|
1335
|
+
for (const [key, value] of Object.entries(parts.frontmatter)) {
|
|
1336
|
+
clauses.push({ [`frontmatter.${key}`]: value });
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (clauses.length === 1) return clauses[0];
|
|
1340
|
+
return { $and: clauses };
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1264
1343
|
// node_modules/zod/v4/classic/external.js
|
|
1265
1344
|
var external_exports = {};
|
|
1266
1345
|
__export(external_exports, {
|
|
@@ -15752,13 +15831,29 @@ var SEARCH_NOTES_DESCRIPTION = [
|
|
|
15752
15831
|
'- "where did I write about X?" \u2192 search_notes({query: "X"}) \u2014 quick.',
|
|
15753
15832
|
'- "what do I know about Y?" \u2192 search_notes({query: "Y", mode: "deep"}).',
|
|
15754
15833
|
'- multilingual pair: search_notes({query: ["embeddings", "\u0432\u0435\u043A\u0442\u043E\u0440\u043D\u0438\u0439 \u043F\u043E\u0448\u0443\u043A"]}) \u2014 returns one merged list; notes matched by both queries appear with both in matched_queries.',
|
|
15755
|
-
'- multilingual deep: search_notes({query: ["optimization", "\u043E\u043F\u0442\u0438\u043C\u0456\u0437\u0430\u0446\u0456\u044F"], mode: "deep"}) \u2014 merged top-`limit` seeds, then expansion once on the merged set.'
|
|
15834
|
+
'- multilingual deep: search_notes({query: ["optimization", "\u043E\u043F\u0442\u0438\u043C\u0456\u0437\u0430\u0446\u0456\u044F"], mode: "deep"}) \u2014 merged top-`limit` seeds, then expansion once on the merged set.',
|
|
15835
|
+
"",
|
|
15836
|
+
"PRE-FILTER (filter parameter):",
|
|
15837
|
+
"- filter: optional structural narrowing applied BEFORE semantic ranking. Best when vault has many narrative notes that crowd top-K on a niche query.",
|
|
15838
|
+
" Shape: { path_prefix?, tags?, frontmatter? }. At least one field required.",
|
|
15839
|
+
' - path_prefix: scope to a folder (e.g. "Resources/").',
|
|
15840
|
+
' - tags: notes that have ANY of these tags (OR within the array; no leading "#").',
|
|
15841
|
+
' - frontmatter: sift filter on frontmatter keys (e.g. { type: "reflection", status: "active" }). Same operator allow-list as query_notes.',
|
|
15842
|
+
" Composition: filter AND threshold AND semantic. Use this instead of querying twice and intersecting on the client.",
|
|
15843
|
+
'- scoped recall: search_notes({query: "trading lessons", filter: {tags: ["trading"]}}) \u2014 semantic only inside notes tagged trading.',
|
|
15844
|
+
'- 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/"}, mode: "deep"}).'
|
|
15756
15845
|
].join("\n");
|
|
15846
|
+
var filterSchema = external_exports.object({
|
|
15847
|
+
path_prefix: external_exports.string().optional(),
|
|
15848
|
+
tags: external_exports.array(external_exports.string()).optional(),
|
|
15849
|
+
frontmatter: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
|
|
15850
|
+
});
|
|
15757
15851
|
var inputSchema4 = external_exports.object({
|
|
15758
15852
|
query: external_exports.union([external_exports.string(), external_exports.array(external_exports.string()).min(1).max(8)]),
|
|
15759
15853
|
mode: external_exports.enum(["quick", "deep"]).optional(),
|
|
15760
15854
|
limit: external_exports.number().int().positive().optional(),
|
|
15761
|
-
threshold: external_exports.number().min(0).max(1).optional()
|
|
15855
|
+
threshold: external_exports.number().min(0).max(1).optional(),
|
|
15856
|
+
filter: filterSchema.optional()
|
|
15762
15857
|
});
|
|
15763
15858
|
async function buildExistingPathSet2(paths, pathExists) {
|
|
15764
15859
|
const unique = new Set(paths);
|
|
@@ -15773,8 +15868,30 @@ function wrapDependencyError2(error48, message, details) {
|
|
|
15773
15868
|
}
|
|
15774
15869
|
return new ToolHandlerError("DEPENDENCY_ERROR", message, { details, cause: error48 });
|
|
15775
15870
|
}
|
|
15871
|
+
function isFilterEmpty(filter) {
|
|
15872
|
+
const hasPath = filter.path_prefix !== void 0 && filter.path_prefix !== "";
|
|
15873
|
+
const hasTags = Array.isArray(filter.tags) && filter.tags.length > 0;
|
|
15874
|
+
const hasFm = filter.frontmatter !== void 0 && Object.keys(filter.frontmatter).length > 0;
|
|
15875
|
+
return !hasPath && !hasTags && !hasFm;
|
|
15876
|
+
}
|
|
15877
|
+
function narrowSources(sources, allowed) {
|
|
15878
|
+
const out = /* @__PURE__ */ new Map();
|
|
15879
|
+
for (const path7 of allowed) {
|
|
15880
|
+
const src = sources.get(path7);
|
|
15881
|
+
if (src) out.set(path7, src);
|
|
15882
|
+
}
|
|
15883
|
+
return out;
|
|
15884
|
+
}
|
|
15776
15885
|
function buildSearchNotesTool(deps) {
|
|
15777
|
-
const {
|
|
15886
|
+
const {
|
|
15887
|
+
sources,
|
|
15888
|
+
embeddingProvider,
|
|
15889
|
+
searchEngine,
|
|
15890
|
+
modelKey,
|
|
15891
|
+
pathExists,
|
|
15892
|
+
graph,
|
|
15893
|
+
listMatchingPaths
|
|
15894
|
+
} = deps;
|
|
15778
15895
|
return {
|
|
15779
15896
|
name: "search_notes",
|
|
15780
15897
|
title: "Search Notes",
|
|
@@ -15784,6 +15901,39 @@ function buildSearchNotesTool(deps) {
|
|
|
15784
15901
|
const mode = input.mode ?? "quick";
|
|
15785
15902
|
const threshold = input.threshold !== void 0 ? readThreshold(input.threshold, input.threshold, "threshold") : void 0;
|
|
15786
15903
|
const limit = input.limit !== void 0 ? readPositiveInteger(input.limit, input.limit, "limit") : void 0;
|
|
15904
|
+
let effectiveSources = sources;
|
|
15905
|
+
if (input.filter !== void 0) {
|
|
15906
|
+
if (isFilterEmpty(input.filter)) {
|
|
15907
|
+
throw new ToolHandlerError(
|
|
15908
|
+
"INVALID_ARGUMENT",
|
|
15909
|
+
"filter must specify at least one of: path_prefix, tags, frontmatter"
|
|
15910
|
+
);
|
|
15911
|
+
}
|
|
15912
|
+
let allowed;
|
|
15913
|
+
try {
|
|
15914
|
+
allowed = await listMatchingPaths(input.filter);
|
|
15915
|
+
} catch (error48) {
|
|
15916
|
+
if (error48 instanceof ToolHandlerError && error48.code === "INVALID_FILTER") {
|
|
15917
|
+
throw new ToolHandlerError("INVALID_ARGUMENT", error48.message, {
|
|
15918
|
+
details: error48.details
|
|
15919
|
+
});
|
|
15920
|
+
}
|
|
15921
|
+
throw wrapDependencyError2(error48, "Failed to compute filter set", {
|
|
15922
|
+
modelKey,
|
|
15923
|
+
operation: "search_notes"
|
|
15924
|
+
});
|
|
15925
|
+
}
|
|
15926
|
+
if (allowed.size === 0) {
|
|
15927
|
+
const isMulti = Array.isArray(input.query);
|
|
15928
|
+
const isDeep = mode === "deep";
|
|
15929
|
+
return {
|
|
15930
|
+
results: [],
|
|
15931
|
+
...isDeep ? { blockResults: [] } : {},
|
|
15932
|
+
...isMulti ? { truncated: false } : {}
|
|
15933
|
+
};
|
|
15934
|
+
}
|
|
15935
|
+
effectiveSources = narrowSources(sources, allowed);
|
|
15936
|
+
}
|
|
15787
15937
|
if (Array.isArray(input.query)) {
|
|
15788
15938
|
const queries = normalizeQueryArray(input.query);
|
|
15789
15939
|
try {
|
|
@@ -15792,7 +15942,7 @@ function buildSearchNotesTool(deps) {
|
|
|
15792
15942
|
mode,
|
|
15793
15943
|
threshold,
|
|
15794
15944
|
limit,
|
|
15795
|
-
sources,
|
|
15945
|
+
sources: effectiveSources,
|
|
15796
15946
|
embeddingProvider,
|
|
15797
15947
|
searchEngine
|
|
15798
15948
|
});
|
|
@@ -15823,7 +15973,7 @@ function buildSearchNotesTool(deps) {
|
|
|
15823
15973
|
mode,
|
|
15824
15974
|
limit,
|
|
15825
15975
|
threshold,
|
|
15826
|
-
sources,
|
|
15976
|
+
sources: effectiveSources,
|
|
15827
15977
|
embeddingProvider,
|
|
15828
15978
|
searchEngine
|
|
15829
15979
|
});
|
|
@@ -15886,10 +16036,12 @@ async function createSemanticModule(config2, deps = {}) {
|
|
|
15886
16036
|
const readNoteContent = deps.readNoteContent ?? createDefaultReadNoteContent(config2.vaultPath);
|
|
15887
16037
|
const basenameIndex = buildBasenameIndex(corpus.sources.keys());
|
|
15888
16038
|
let graph = deps.graph;
|
|
15889
|
-
|
|
16039
|
+
let listMatchingPaths = deps.listMatchingPaths;
|
|
16040
|
+
if (!graph || !listMatchingPaths) {
|
|
15890
16041
|
const readerFactory = deps.vaultReaderFactory ?? ((opts) => new FsVaultReader({ vaultRoot: opts.vaultRoot }));
|
|
15891
16042
|
const reader = readerFactory({ vaultRoot: config2.vaultPath });
|
|
15892
|
-
graph = new WikilinkGraphIndex({ reader });
|
|
16043
|
+
if (!graph) graph = new WikilinkGraphIndex({ reader });
|
|
16044
|
+
if (!listMatchingPaths) listMatchingPaths = createListMatchingPaths({ reader, graph });
|
|
15893
16045
|
}
|
|
15894
16046
|
const semanticDeps = {
|
|
15895
16047
|
sources: corpus.sources,
|
|
@@ -15899,7 +16051,8 @@ async function createSemanticModule(config2, deps = {}) {
|
|
|
15899
16051
|
pathExists,
|
|
15900
16052
|
basenameIndex,
|
|
15901
16053
|
readNoteContent,
|
|
15902
|
-
graph
|
|
16054
|
+
graph,
|
|
16055
|
+
listMatchingPaths
|
|
15903
16056
|
};
|
|
15904
16057
|
return {
|
|
15905
16058
|
tools: buildSemanticTools(semanticDeps),
|
|
@@ -16311,7 +16464,7 @@ async function resolveToPath(input, reader) {
|
|
|
16311
16464
|
if (matches.length > 1) {
|
|
16312
16465
|
throw new ToolHandlerError(
|
|
16313
16466
|
"AMBIGUOUS_MATCH",
|
|
16314
|
-
`Multiple notes match name '${name}'; pass an explicit path`,
|
|
16467
|
+
`Multiple notes match name '${name}': ${matches.join(", ")}; pass an explicit path`,
|
|
16315
16468
|
{ details: { name, matches } }
|
|
16316
16469
|
);
|
|
16317
16470
|
}
|
|
@@ -16595,7 +16748,7 @@ var FsVaultWriter = class {
|
|
|
16595
16748
|
}
|
|
16596
16749
|
throw new ToolHandlerError(
|
|
16597
16750
|
"AMBIGUOUS_MATCH",
|
|
16598
|
-
`Find text matched ${result.lines.length} times in ${input.path};
|
|
16751
|
+
`Find text matched ${result.lines.length} times in ${input.path} at lines ${result.lines.join(", ")}; make 'replace' more specific (extend the anchor with surrounding text) or omit it to rewrite the whole body`,
|
|
16599
16752
|
{ details: { path: input.path, matches: result.lines } }
|
|
16600
16753
|
);
|
|
16601
16754
|
}
|
|
@@ -16703,6 +16856,10 @@ Use \`search_notes\` when the user is recalling a topic fuzzily, asking a concep
|
|
|
16703
16856
|
3. The vault may contain notes written in several languages. If you have evidence of which languages are in use (from prior reads, file names, or earlier results), include translations of the key concepts into each of those languages in the same \`query\` array.
|
|
16704
16857
|
4. If a search returns no results, lower the threshold to 0.3 before giving up.
|
|
16705
16858
|
|
|
16859
|
+
### 1a. Narrow scope when domain is known
|
|
16860
|
+
|
|
16861
|
+
If the query is about a known folder, tag, or frontmatter dimension, pass \`filter\` to scope semantic search to that subset BEFORE ranking. This dramatically improves precision when the vault has many narrative notes that crowd top-K. Shape: \`{ path_prefix?, tags?, frontmatter? }\` (at least one). Tags match ANY-of (OR). \`frontmatter\` accepts the same sift operators as \`query_notes\`. Examples: \`filter: { tags: ['trading'] }\`, \`filter: { path_prefix: 'Resources/', frontmatter: { type: 'reflection' } }\`.
|
|
16862
|
+
|
|
16706
16863
|
### 2. Choose mode
|
|
16707
16864
|
- **quick** (default) \u2014 returns up to 3 notes plus block-level matches scoped to those notes. Use for specific lookups.
|
|
16708
16865
|
- **deep** \u2014 returns up to 8 notes plus block-level matches across the whole vault. After the merged top-\`limit\` seeds are selected, expansion runs once on those seeds to pull in related notes. Use for broad topics.
|
|
@@ -16740,6 +16897,10 @@ async function startNeuroVaultServer(config2, deps = {}) {
|
|
|
16740
16897
|
const server = serverFactory();
|
|
16741
16898
|
const sharedReader = new FsVaultReader({ vaultRoot: config2.vaultPath });
|
|
16742
16899
|
const sharedGraph = new WikilinkGraphIndex({ reader: sharedReader });
|
|
16900
|
+
const sharedListMatchingPaths = createListMatchingPaths({
|
|
16901
|
+
reader: sharedReader,
|
|
16902
|
+
graph: sharedGraph
|
|
16903
|
+
});
|
|
16743
16904
|
const registrations = [];
|
|
16744
16905
|
let warmup = async () => {
|
|
16745
16906
|
};
|
|
@@ -16751,7 +16912,11 @@ async function startNeuroVaultServer(config2, deps = {}) {
|
|
|
16751
16912
|
modelKey: config2.semantic.modelKey,
|
|
16752
16913
|
modelId: config2.semantic.modelId
|
|
16753
16914
|
},
|
|
16754
|
-
{
|
|
16915
|
+
{
|
|
16916
|
+
graph: sharedGraph,
|
|
16917
|
+
listMatchingPaths: sharedListMatchingPaths,
|
|
16918
|
+
...deps.semantic
|
|
16919
|
+
}
|
|
16755
16920
|
);
|
|
16756
16921
|
registrations.push(...semantic.tools);
|
|
16757
16922
|
warmup = semantic.warmup;
|