neuro-vault-mcp 1.6.0 → 1.7.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/dist/cli.d.ts +16 -2
- package/dist/cli.js +230 -25
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ interface RetrievalOutput {
|
|
|
6
6
|
results: SearchResult[];
|
|
7
7
|
blockResults?: BlockSearchResult[];
|
|
8
8
|
}
|
|
9
|
+
interface MultiRetrievalOutput {
|
|
10
|
+
results: MultiSearchResult[];
|
|
11
|
+
blockResults?: MultiBlockSearchResult[];
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
interface SmartBlock {
|
|
11
16
|
key: string;
|
|
@@ -58,7 +63,7 @@ interface SearchEngine {
|
|
|
58
63
|
}
|
|
59
64
|
type SearchMode = 'quick' | 'deep';
|
|
60
65
|
interface SearchNotesInput {
|
|
61
|
-
query: string;
|
|
66
|
+
query: string | string[];
|
|
62
67
|
mode?: SearchMode;
|
|
63
68
|
limit?: number;
|
|
64
69
|
threshold?: number;
|
|
@@ -79,6 +84,7 @@ interface ToolStats {
|
|
|
79
84
|
embeddingDimension: number;
|
|
80
85
|
modelKey: string;
|
|
81
86
|
}
|
|
87
|
+
type PathExistsCheck = (vaultRelativePath: string) => Promise<boolean>;
|
|
82
88
|
interface ToolHandlerDependencies {
|
|
83
89
|
loader: {
|
|
84
90
|
sources: Map<string, SmartSource>;
|
|
@@ -86,13 +92,20 @@ interface ToolHandlerDependencies {
|
|
|
86
92
|
embeddingProvider: EmbeddingProvider;
|
|
87
93
|
searchEngine: SearchEngine;
|
|
88
94
|
modelKey: string;
|
|
95
|
+
pathExists?: PathExistsCheck;
|
|
89
96
|
}
|
|
90
97
|
interface ToolHandlers {
|
|
91
|
-
searchNotes(input: SearchNotesInput): Promise<RetrievalOutput>;
|
|
98
|
+
searchNotes(input: SearchNotesInput): Promise<RetrievalOutput | MultiRetrievalOutput>;
|
|
92
99
|
getSimilarNotes(input: GetSimilarNotesInput): Promise<SearchResult[]>;
|
|
93
100
|
findDuplicates(input?: FindDuplicatesInput): Promise<DuplicatePair[]>;
|
|
94
101
|
getStats(): Promise<ToolStats>;
|
|
95
102
|
}
|
|
103
|
+
interface MultiSearchResult extends SearchResult {
|
|
104
|
+
matched_queries: string[];
|
|
105
|
+
}
|
|
106
|
+
interface MultiBlockSearchResult extends BlockSearchResult {
|
|
107
|
+
matched_queries: string[];
|
|
108
|
+
}
|
|
96
109
|
|
|
97
110
|
interface SmartConnectionsCorpus {
|
|
98
111
|
sources: Map<string, SmartSource>;
|
|
@@ -103,6 +116,7 @@ interface SemanticModuleDeps {
|
|
|
103
116
|
embeddingServiceFactory?: (modelId: string) => EmbeddingProvider;
|
|
104
117
|
searchEngine?: SearchEngine;
|
|
105
118
|
toolHandlersFactory?: (deps: ToolHandlerDependencies) => ToolHandlers;
|
|
119
|
+
pathExists?: PathExistsCheck;
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
type NoteIdentifier = {
|
package/dist/cli.js
CHANGED
|
@@ -61,6 +61,10 @@ import { createRequire } from "module";
|
|
|
61
61
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
62
62
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
63
63
|
|
|
64
|
+
// src/modules/semantic/index.ts
|
|
65
|
+
import fs2 from "fs/promises";
|
|
66
|
+
import path4 from "path";
|
|
67
|
+
|
|
64
68
|
// src/modules/semantic/embedding-service.ts
|
|
65
69
|
import { pipeline } from "@xenova/transformers";
|
|
66
70
|
var DEFAULT_MODEL_KEY2 = "bge-micro-v2";
|
|
@@ -496,6 +500,7 @@ import path3 from "path";
|
|
|
496
500
|
// src/modules/semantic/retrieval-policy.ts
|
|
497
501
|
var FALLBACK_THRESHOLD = 0.3;
|
|
498
502
|
var QUICK_BLOCK_LIMIT = 5;
|
|
503
|
+
var HARD_MERGE_CAP = 50;
|
|
499
504
|
var MODE_DEFAULTS = {
|
|
500
505
|
quick: { limit: 3, threshold: 0.5, expansion: false, expansionLimit: 0 },
|
|
501
506
|
deep: { limit: 8, threshold: 0.35, expansion: true, expansionLimit: 3 }
|
|
@@ -570,6 +575,97 @@ async function executeRetrieval(input) {
|
|
|
570
575
|
...blockResults !== void 0 ? { blockResults } : {}
|
|
571
576
|
};
|
|
572
577
|
}
|
|
578
|
+
function mergeNoteResults(perQuery) {
|
|
579
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
580
|
+
for (const { query, results } of perQuery) {
|
|
581
|
+
for (const result of results) {
|
|
582
|
+
const existing = byPath.get(result.path);
|
|
583
|
+
if (!existing) {
|
|
584
|
+
byPath.set(result.path, {
|
|
585
|
+
path: result.path,
|
|
586
|
+
similarity: result.similarity,
|
|
587
|
+
matched_queries: [query]
|
|
588
|
+
});
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (result.similarity > existing.similarity) {
|
|
592
|
+
existing.similarity = result.similarity;
|
|
593
|
+
}
|
|
594
|
+
if (!existing.matched_queries.includes(query)) {
|
|
595
|
+
existing.matched_queries.push(query);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return [...byPath.values()].sort(
|
|
600
|
+
(a, b) => b.similarity - a.similarity || a.path.localeCompare(b.path)
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
function mergeBlockResults(perQuery) {
|
|
604
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const { query, blocks } of perQuery) {
|
|
606
|
+
for (const block of blocks) {
|
|
607
|
+
const key = `${block.path}\0${block.heading}\0${block.lines[0]}-${block.lines[1]}`;
|
|
608
|
+
const existing = byKey.get(key);
|
|
609
|
+
if (!existing) {
|
|
610
|
+
byKey.set(key, {
|
|
611
|
+
path: block.path,
|
|
612
|
+
heading: block.heading,
|
|
613
|
+
lines: block.lines,
|
|
614
|
+
similarity: block.similarity,
|
|
615
|
+
matched_queries: [query]
|
|
616
|
+
});
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (block.similarity > existing.similarity) {
|
|
620
|
+
existing.similarity = block.similarity;
|
|
621
|
+
}
|
|
622
|
+
if (!existing.matched_queries.includes(query)) {
|
|
623
|
+
existing.matched_queries.push(query);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return [...byKey.values()].sort(
|
|
628
|
+
(a, b) => b.similarity - a.similarity || a.path.localeCompare(b.path)
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
async function executeMultiRetrieval(input) {
|
|
632
|
+
const { queries, mode, sources, embeddingProvider, searchEngine } = input;
|
|
633
|
+
const perQueryOutputs = await Promise.all(
|
|
634
|
+
queries.map(async (query) => {
|
|
635
|
+
const output = await executeRetrieval({
|
|
636
|
+
query,
|
|
637
|
+
mode,
|
|
638
|
+
threshold: input.threshold,
|
|
639
|
+
expansion: input.expansion,
|
|
640
|
+
expansionLimit: input.expansionLimit,
|
|
641
|
+
sources,
|
|
642
|
+
embeddingProvider,
|
|
643
|
+
searchEngine
|
|
644
|
+
});
|
|
645
|
+
return { query, output };
|
|
646
|
+
})
|
|
647
|
+
);
|
|
648
|
+
const mergedNotes = mergeNoteResults(
|
|
649
|
+
perQueryOutputs.map(({ query, output }) => ({ query, results: output.results }))
|
|
650
|
+
);
|
|
651
|
+
const anyBlocks = perQueryOutputs.some(({ output }) => output.blockResults !== void 0);
|
|
652
|
+
const mergedBlocks = anyBlocks ? mergeBlockResults(
|
|
653
|
+
perQueryOutputs.map(({ query, output }) => ({
|
|
654
|
+
query,
|
|
655
|
+
blocks: output.blockResults ?? []
|
|
656
|
+
}))
|
|
657
|
+
) : void 0;
|
|
658
|
+
const perQueryLimit = input.limit ?? MODE_DEFAULTS[mode].limit;
|
|
659
|
+
const cap = Math.min(perQueryLimit * queries.length, HARD_MERGE_CAP);
|
|
660
|
+
const truncated = mergedNotes.length > cap || (mergedBlocks?.length ?? 0) > cap;
|
|
661
|
+
const cappedNotes = mergedNotes.slice(0, cap);
|
|
662
|
+
const cappedBlocks = mergedBlocks ? mergedBlocks.slice(0, cap) : void 0;
|
|
663
|
+
return {
|
|
664
|
+
results: cappedNotes,
|
|
665
|
+
...cappedBlocks !== void 0 ? { blockResults: cappedBlocks } : {},
|
|
666
|
+
truncated
|
|
667
|
+
};
|
|
668
|
+
}
|
|
573
669
|
|
|
574
670
|
// src/lib/tool-response.ts
|
|
575
671
|
var ToolHandlerError = class extends Error {
|
|
@@ -624,6 +720,7 @@ var DEFAULT_SEARCH_LIMIT = 10;
|
|
|
624
720
|
var DEFAULT_SEARCH_THRESHOLD = 0.5;
|
|
625
721
|
var DEFAULT_DUPLICATE_THRESHOLD = 0.9;
|
|
626
722
|
var WINDOWS_ABSOLUTE_PATH_RE = /^[A-Za-z]:[\\/]/;
|
|
723
|
+
var MAX_MULTI_QUERIES = 8;
|
|
627
724
|
function normalizeNotePath(notePath) {
|
|
628
725
|
const trimmed = notePath.trim();
|
|
629
726
|
if (!trimmed) {
|
|
@@ -664,6 +761,40 @@ function normalizeQuery(query) {
|
|
|
664
761
|
}
|
|
665
762
|
return normalized;
|
|
666
763
|
}
|
|
764
|
+
function normalizeQueryArray(queries) {
|
|
765
|
+
if (queries.length === 0) {
|
|
766
|
+
throw new ToolHandlerError("INVALID_ARGUMENT", "query array must not be empty", {
|
|
767
|
+
details: { field: "query" }
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
if (queries.length > MAX_MULTI_QUERIES) {
|
|
771
|
+
throw new ToolHandlerError(
|
|
772
|
+
"INVALID_ARGUMENT",
|
|
773
|
+
`query array must contain at most ${MAX_MULTI_QUERIES} entries`,
|
|
774
|
+
{ details: { field: "query", max: MAX_MULTI_QUERIES, received: queries.length } }
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
const seen = /* @__PURE__ */ new Set();
|
|
778
|
+
const out = [];
|
|
779
|
+
for (const raw of queries) {
|
|
780
|
+
if (typeof raw !== "string") {
|
|
781
|
+
throw new ToolHandlerError("INVALID_ARGUMENT", "query array must contain strings", {
|
|
782
|
+
details: { field: "query" }
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const trimmed = raw.trim();
|
|
786
|
+
if (trimmed.length === 0) {
|
|
787
|
+
throw new ToolHandlerError("INVALID_ARGUMENT", "query entries must not be empty", {
|
|
788
|
+
details: { field: "query" }
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
if (!seen.has(trimmed)) {
|
|
792
|
+
seen.add(trimmed);
|
|
793
|
+
out.push(trimmed);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return out;
|
|
797
|
+
}
|
|
667
798
|
function readPositiveInteger(value, defaultValue, field) {
|
|
668
799
|
if (value === void 0) {
|
|
669
800
|
return defaultValue;
|
|
@@ -732,20 +863,61 @@ function wrapDependencyError(error48, message, details) {
|
|
|
732
863
|
cause: error48
|
|
733
864
|
});
|
|
734
865
|
}
|
|
866
|
+
async function buildExistingPathSet(paths, pathExists) {
|
|
867
|
+
const unique = new Set(paths);
|
|
868
|
+
const checks = await Promise.all(
|
|
869
|
+
[...unique].map(async (notePath) => [notePath, await pathExists(notePath)])
|
|
870
|
+
);
|
|
871
|
+
return new Set(checks.filter(([, exists]) => exists).map(([notePath]) => notePath));
|
|
872
|
+
}
|
|
735
873
|
function createToolHandlers({
|
|
736
874
|
loader,
|
|
737
875
|
embeddingProvider,
|
|
738
876
|
searchEngine,
|
|
739
|
-
modelKey
|
|
877
|
+
modelKey,
|
|
878
|
+
pathExists
|
|
740
879
|
}) {
|
|
880
|
+
const existsCheck = pathExists ?? (async () => true);
|
|
741
881
|
return {
|
|
742
882
|
async searchNotes(input) {
|
|
743
|
-
const query = normalizeQuery(input.query);
|
|
744
883
|
const mode = input.mode ?? "quick";
|
|
745
884
|
const threshold = input.threshold !== void 0 ? readThreshold(input.threshold, input.threshold, "threshold") : void 0;
|
|
746
885
|
const expansionLimit = input.expansion_limit !== void 0 ? readPositiveInteger(input.expansion_limit, 3, "expansion_limit") : void 0;
|
|
886
|
+
if (Array.isArray(input.query)) {
|
|
887
|
+
const queries = normalizeQueryArray(input.query);
|
|
888
|
+
const limit = input.limit !== void 0 ? readPositiveInteger(input.limit, input.limit, "limit") : void 0;
|
|
889
|
+
try {
|
|
890
|
+
const output = await executeMultiRetrieval({
|
|
891
|
+
queries,
|
|
892
|
+
mode,
|
|
893
|
+
threshold,
|
|
894
|
+
expansion: input.expansion,
|
|
895
|
+
expansionLimit,
|
|
896
|
+
limit,
|
|
897
|
+
sources: loader.sources,
|
|
898
|
+
embeddingProvider,
|
|
899
|
+
searchEngine
|
|
900
|
+
});
|
|
901
|
+
const candidatePaths = [
|
|
902
|
+
...output.results.map((r) => r.path),
|
|
903
|
+
...output.blockResults?.map((b) => b.path) ?? []
|
|
904
|
+
];
|
|
905
|
+
const existing = await buildExistingPathSet(candidatePaths, existsCheck);
|
|
906
|
+
return {
|
|
907
|
+
results: output.results.filter((r) => existing.has(r.path)),
|
|
908
|
+
...output.blockResults !== void 0 ? { blockResults: output.blockResults.filter((b) => existing.has(b.path)) } : {},
|
|
909
|
+
truncated: output.truncated
|
|
910
|
+
};
|
|
911
|
+
} catch (error48) {
|
|
912
|
+
throw wrapDependencyError(error48, "Failed to search notes", {
|
|
913
|
+
modelKey,
|
|
914
|
+
operation: "search_notes"
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const query = normalizeQuery(input.query);
|
|
747
919
|
try {
|
|
748
|
-
|
|
920
|
+
const output = await executeRetrieval({
|
|
749
921
|
query,
|
|
750
922
|
mode,
|
|
751
923
|
threshold,
|
|
@@ -755,6 +927,15 @@ function createToolHandlers({
|
|
|
755
927
|
embeddingProvider,
|
|
756
928
|
searchEngine
|
|
757
929
|
});
|
|
930
|
+
const candidatePaths = [
|
|
931
|
+
...output.results.map((r) => r.path),
|
|
932
|
+
...output.blockResults?.map((b) => b.path) ?? []
|
|
933
|
+
];
|
|
934
|
+
const existing = await buildExistingPathSet(candidatePaths, existsCheck);
|
|
935
|
+
return {
|
|
936
|
+
results: output.results.filter((r) => existing.has(r.path)),
|
|
937
|
+
...output.blockResults !== void 0 ? { blockResults: output.blockResults.filter((b) => existing.has(b.path)) } : {}
|
|
938
|
+
};
|
|
758
939
|
} catch (error48) {
|
|
759
940
|
throw wrapDependencyError(error48, "Failed to search notes", {
|
|
760
941
|
modelKey,
|
|
@@ -773,7 +954,7 @@ function createToolHandlers({
|
|
|
773
954
|
const limit = readPositiveInteger(input.limit, DEFAULT_SEARCH_LIMIT, "limit");
|
|
774
955
|
const threshold = readThreshold(input.threshold, DEFAULT_SEARCH_THRESHOLD, "threshold");
|
|
775
956
|
try {
|
|
776
|
-
|
|
957
|
+
const results = toSearchResults2(
|
|
777
958
|
searchEngine,
|
|
778
959
|
source.embedding,
|
|
779
960
|
loader.sources.values(),
|
|
@@ -781,6 +962,11 @@ function createToolHandlers({
|
|
|
781
962
|
limit,
|
|
782
963
|
notePath
|
|
783
964
|
);
|
|
965
|
+
const existing = await buildExistingPathSet(
|
|
966
|
+
results.map((r) => r.path),
|
|
967
|
+
existsCheck
|
|
968
|
+
);
|
|
969
|
+
return results.filter((r) => existing.has(r.path));
|
|
784
970
|
} catch (error48) {
|
|
785
971
|
throw wrapDependencyError(error48, "Failed to find similar notes", {
|
|
786
972
|
modelKey,
|
|
@@ -792,10 +978,15 @@ function createToolHandlers({
|
|
|
792
978
|
async findDuplicates(input = {}) {
|
|
793
979
|
const threshold = readThreshold(input.threshold, DEFAULT_DUPLICATE_THRESHOLD, "threshold");
|
|
794
980
|
try {
|
|
795
|
-
|
|
981
|
+
const pairs = searchEngine.findDuplicates({
|
|
796
982
|
sources: loader.sources.values(),
|
|
797
983
|
threshold
|
|
798
984
|
});
|
|
985
|
+
const existing = await buildExistingPathSet(
|
|
986
|
+
pairs.flatMap((p) => [p.note_a, p.note_b]),
|
|
987
|
+
existsCheck
|
|
988
|
+
);
|
|
989
|
+
return pairs.filter((p) => existing.has(p.note_a) && existing.has(p.note_b));
|
|
799
990
|
} catch (error48) {
|
|
800
991
|
throw wrapDependencyError(error48, "Failed to find duplicate notes", {
|
|
801
992
|
modelKey,
|
|
@@ -1585,10 +1776,10 @@ function mergeDefs(...defs) {
|
|
|
1585
1776
|
function cloneDef(schema) {
|
|
1586
1777
|
return mergeDefs(schema._zod.def);
|
|
1587
1778
|
}
|
|
1588
|
-
function getElementAtPath(obj,
|
|
1589
|
-
if (!
|
|
1779
|
+
function getElementAtPath(obj, path5) {
|
|
1780
|
+
if (!path5)
|
|
1590
1781
|
return obj;
|
|
1591
|
-
return
|
|
1782
|
+
return path5.reduce((acc, key) => acc?.[key], obj);
|
|
1592
1783
|
}
|
|
1593
1784
|
function promiseAllObject(promisesObj) {
|
|
1594
1785
|
const keys = Object.keys(promisesObj);
|
|
@@ -1971,11 +2162,11 @@ function aborted(x, startIndex = 0) {
|
|
|
1971
2162
|
}
|
|
1972
2163
|
return false;
|
|
1973
2164
|
}
|
|
1974
|
-
function prefixIssues(
|
|
2165
|
+
function prefixIssues(path5, issues) {
|
|
1975
2166
|
return issues.map((iss) => {
|
|
1976
2167
|
var _a2;
|
|
1977
2168
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
1978
|
-
iss.path.unshift(
|
|
2169
|
+
iss.path.unshift(path5);
|
|
1979
2170
|
return iss;
|
|
1980
2171
|
});
|
|
1981
2172
|
}
|
|
@@ -2158,7 +2349,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
2158
2349
|
}
|
|
2159
2350
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
2160
2351
|
const result = { errors: [] };
|
|
2161
|
-
const processError = (error49,
|
|
2352
|
+
const processError = (error49, path5 = []) => {
|
|
2162
2353
|
var _a2, _b;
|
|
2163
2354
|
for (const issue2 of error49.issues) {
|
|
2164
2355
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -2168,7 +2359,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
2168
2359
|
} else if (issue2.code === "invalid_element") {
|
|
2169
2360
|
processError({ issues: issue2.issues }, issue2.path);
|
|
2170
2361
|
} else {
|
|
2171
|
-
const fullpath = [...
|
|
2362
|
+
const fullpath = [...path5, ...issue2.path];
|
|
2172
2363
|
if (fullpath.length === 0) {
|
|
2173
2364
|
result.errors.push(mapper(issue2));
|
|
2174
2365
|
continue;
|
|
@@ -2200,8 +2391,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
2200
2391
|
}
|
|
2201
2392
|
function toDotPath(_path) {
|
|
2202
2393
|
const segs = [];
|
|
2203
|
-
const
|
|
2204
|
-
for (const seg of
|
|
2394
|
+
const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
2395
|
+
for (const seg of path5) {
|
|
2205
2396
|
if (typeof seg === "number")
|
|
2206
2397
|
segs.push(`[${seg}]`);
|
|
2207
2398
|
else if (typeof seg === "symbol")
|
|
@@ -14178,13 +14369,13 @@ function resolveRef(ref, ctx) {
|
|
|
14178
14369
|
if (!ref.startsWith("#")) {
|
|
14179
14370
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
14180
14371
|
}
|
|
14181
|
-
const
|
|
14182
|
-
if (
|
|
14372
|
+
const path5 = ref.slice(1).split("/").filter(Boolean);
|
|
14373
|
+
if (path5.length === 0) {
|
|
14183
14374
|
return ctx.rootSchema;
|
|
14184
14375
|
}
|
|
14185
14376
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
14186
|
-
if (
|
|
14187
|
-
const key =
|
|
14377
|
+
if (path5[0] === defsKey) {
|
|
14378
|
+
const key = path5[1];
|
|
14188
14379
|
if (!key || !ctx.defs[key]) {
|
|
14189
14380
|
throw new Error(`Reference not found: ${ref}`);
|
|
14190
14381
|
}
|
|
@@ -14588,7 +14779,7 @@ config(en_default());
|
|
|
14588
14779
|
|
|
14589
14780
|
// src/modules/semantic/tools.ts
|
|
14590
14781
|
var searchNotesSchema = external_exports.object({
|
|
14591
|
-
query: external_exports.string(),
|
|
14782
|
+
query: external_exports.union([external_exports.string(), external_exports.array(external_exports.string()).min(1).max(8)]),
|
|
14592
14783
|
mode: external_exports.enum(["quick", "deep"]).optional(),
|
|
14593
14784
|
limit: external_exports.number().int().positive().optional(),
|
|
14594
14785
|
threshold: external_exports.number().min(0).max(1).optional(),
|
|
@@ -14609,7 +14800,7 @@ function buildSemanticTools(handlers) {
|
|
|
14609
14800
|
name: "search_notes",
|
|
14610
14801
|
spec: {
|
|
14611
14802
|
title: "Search Notes",
|
|
14612
|
-
description: 'Search notes by semantic similarity for fuzzy recall, topic lookup, or cross-language matching. Pass a short keyword query (1-4 words). Choose mode: "quick" for specific lookups (up to 3 notes), "deep" for broad topic overview with block-level search and expansion.
|
|
14803
|
+
description: 'Search notes by semantic similarity for fuzzy recall, topic lookup, or cross-language matching. Pass a short keyword query (1-4 words). For synonyms, reformulations, or translations into the languages used in the vault, pass an array of 1-8 queries \u2014 they are batch-embedded server-side and returned as one merged ranked list with matched_queries on each result. Choose mode: "quick" for specific lookups (up to 3 notes), "deep" for broad topic overview with block-level search and expansion.',
|
|
14613
14804
|
inputSchema: searchNotesSchema
|
|
14614
14805
|
},
|
|
14615
14806
|
handler: async (args) => invokeTool(() => handlers.searchNotes(searchNotesSchema.parse(args)))
|
|
@@ -14644,6 +14835,16 @@ function buildSemanticTools(handlers) {
|
|
|
14644
14835
|
}
|
|
14645
14836
|
|
|
14646
14837
|
// src/modules/semantic/index.ts
|
|
14838
|
+
function createDefaultPathExists(vaultPath) {
|
|
14839
|
+
return async (vaultRelativePath) => {
|
|
14840
|
+
try {
|
|
14841
|
+
await fs2.access(path4.join(vaultPath, vaultRelativePath));
|
|
14842
|
+
return true;
|
|
14843
|
+
} catch {
|
|
14844
|
+
return false;
|
|
14845
|
+
}
|
|
14846
|
+
};
|
|
14847
|
+
}
|
|
14647
14848
|
async function createSemanticModule(config2, deps = {}) {
|
|
14648
14849
|
const loadCorpus = deps.loadCorpus ?? loadSmartConnectionsCorpus;
|
|
14649
14850
|
const embeddingServiceFactory = deps.embeddingServiceFactory ?? ((modelId) => new EmbeddingService({ modelKey: modelId }));
|
|
@@ -14654,11 +14855,13 @@ async function createSemanticModule(config2, deps = {}) {
|
|
|
14654
14855
|
throw new Error("Loaded Smart Connections corpus is empty");
|
|
14655
14856
|
}
|
|
14656
14857
|
const embeddingService = embeddingServiceFactory(config2.modelId);
|
|
14858
|
+
const pathExists = deps.pathExists ?? createDefaultPathExists(config2.vaultPath);
|
|
14657
14859
|
const handlers = toolHandlersFactory({
|
|
14658
14860
|
loader: corpus,
|
|
14659
14861
|
embeddingProvider: embeddingService,
|
|
14660
14862
|
searchEngine,
|
|
14661
|
-
modelKey: config2.modelKey
|
|
14863
|
+
modelKey: config2.modelKey,
|
|
14864
|
+
pathExists
|
|
14662
14865
|
});
|
|
14663
14866
|
return {
|
|
14664
14867
|
tools: buildSemanticTools(handlers),
|
|
@@ -15313,10 +15516,9 @@ Use \`search_notes\` when the user is recalling a topic fuzzily, asking a concep
|
|
|
15313
15516
|
|
|
15314
15517
|
### 1. Write the query
|
|
15315
15518
|
1. Extract the core nouns and concepts from the user's message \u2014 strip filler words and verbs. From "remind me what I wanted to build with LLM agents" the key concepts are "LLM", "agents", "build".
|
|
15316
|
-
2.
|
|
15317
|
-
3.
|
|
15318
|
-
4.
|
|
15319
|
-
5. If a search returns no results, lower the threshold to 0.3 before giving up.
|
|
15519
|
+
2. For synonyms, reformulations, or translations, pass \`query\` as an array of 1-8 strings in a SINGLE call \u2014 the server batch-embeds them and returns one merged ranked list with \`matched_queries\` showing which query hit which note. Do not make multiple sequential calls for synonyms.
|
|
15520
|
+
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.
|
|
15521
|
+
4. If a search returns no results, lower the threshold to 0.3 before giving up.
|
|
15320
15522
|
|
|
15321
15523
|
### 2. Choose mode
|
|
15322
15524
|
- **quick** (default) \u2014 returns up to 3 notes, no expansion. Use for specific lookups.
|
|
@@ -15325,6 +15527,8 @@ Use \`search_notes\` when the user is recalling a topic fuzzily, asking a concep
|
|
|
15325
15527
|
|
|
15326
15528
|
### 3. Use the results
|
|
15327
15529
|
- \`results\` \u2014 notes ranked by similarity; read the file by path
|
|
15530
|
+
- \`matched_queries\` (only when \`query\` is an array) \u2014 which of your queries hit this note; lets you spot which synonym was load-bearing
|
|
15531
|
+
- \`truncated\` (only when \`query\` is an array) \u2014 true when more candidates were merged than fit in the cap
|
|
15328
15532
|
- \`blockResults\` \u2014 sections ranked by relevance; use heading + line range to jump to the relevant part
|
|
15329
15533
|
- After finding a relevant note, call get_similar_notes to discover related content
|
|
15330
15534
|
|
|
@@ -15356,6 +15560,7 @@ async function startNeuroVaultServer(config2, deps = {}) {
|
|
|
15356
15560
|
if (config2.semantic.enabled) {
|
|
15357
15561
|
const semantic = await createSemanticModule(
|
|
15358
15562
|
{
|
|
15563
|
+
vaultPath: config2.vaultPath,
|
|
15359
15564
|
smartEnvPath: config2.semantic.smartEnvPath,
|
|
15360
15565
|
modelKey: config2.semantic.modelKey,
|
|
15361
15566
|
modelId: config2.semantic.modelId
|