neuro-vault-mcp 1.6.0 → 1.7.2
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 +18 -2
- package/dist/cli.js +292 -38
- package/dist/cli.js.map +1 -1
- package/package.json +3 -2
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 = {
|
|
@@ -117,6 +131,7 @@ interface ReadNoteInput {
|
|
|
117
131
|
}
|
|
118
132
|
interface ReadNoteResult {
|
|
119
133
|
path: string;
|
|
134
|
+
frontmatter: Record<string, unknown> | null;
|
|
120
135
|
content: string;
|
|
121
136
|
}
|
|
122
137
|
interface CreateNoteInput {
|
|
@@ -137,6 +152,7 @@ interface EditNoteInput {
|
|
|
137
152
|
}
|
|
138
153
|
interface DailyNoteResult {
|
|
139
154
|
path: string;
|
|
155
|
+
frontmatter: Record<string, unknown> | null;
|
|
140
156
|
content: string;
|
|
141
157
|
}
|
|
142
158
|
interface AppendDailyInput {
|
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),
|
|
@@ -14671,9 +14874,47 @@ async function createSemanticModule(config2, deps = {}) {
|
|
|
14671
14874
|
// src/modules/operations/obsidian-cli-provider.ts
|
|
14672
14875
|
import { execFile } from "child_process";
|
|
14673
14876
|
import { promisify } from "util";
|
|
14877
|
+
|
|
14878
|
+
// src/modules/operations/frontmatter.ts
|
|
14879
|
+
import { parse as parseYaml } from "yaml";
|
|
14880
|
+
var FRONTMATTER_OPEN = /^---[ \t]*\r?\n/;
|
|
14881
|
+
var FRONTMATTER_CLOSE = /(^|\r?\n)---[ \t]*(\r?\n|$)/;
|
|
14882
|
+
function splitFrontmatter(raw) {
|
|
14883
|
+
const openMatch = raw.match(FRONTMATTER_OPEN);
|
|
14884
|
+
if (!openMatch) {
|
|
14885
|
+
return { frontmatter: null, content: raw };
|
|
14886
|
+
}
|
|
14887
|
+
const afterOpen = raw.slice(openMatch[0].length);
|
|
14888
|
+
const closeMatch = afterOpen.match(FRONTMATTER_CLOSE);
|
|
14889
|
+
if (!closeMatch || closeMatch.index === void 0) {
|
|
14890
|
+
return { frontmatter: null, content: raw };
|
|
14891
|
+
}
|
|
14892
|
+
const yamlBody = afterOpen.slice(0, closeMatch.index);
|
|
14893
|
+
const content = afterOpen.slice(closeMatch.index + closeMatch[0].length);
|
|
14894
|
+
let parsed;
|
|
14895
|
+
try {
|
|
14896
|
+
parsed = parseYaml(yamlBody);
|
|
14897
|
+
} catch (err) {
|
|
14898
|
+
console.warn(
|
|
14899
|
+
`[neuro-vault] frontmatter YAML parse failed: ${err.message}; returning raw content`
|
|
14900
|
+
);
|
|
14901
|
+
return { frontmatter: null, content: raw };
|
|
14902
|
+
}
|
|
14903
|
+
if (parsed === null || parsed === void 0) {
|
|
14904
|
+
return { frontmatter: {}, content };
|
|
14905
|
+
}
|
|
14906
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14907
|
+
console.warn(
|
|
14908
|
+
`[neuro-vault] frontmatter parsed to non-object (${typeof parsed}); returning raw content`
|
|
14909
|
+
);
|
|
14910
|
+
return { frontmatter: null, content: raw };
|
|
14911
|
+
}
|
|
14912
|
+
return { frontmatter: parsed, content };
|
|
14913
|
+
}
|
|
14914
|
+
|
|
14915
|
+
// src/modules/operations/obsidian-cli-provider.ts
|
|
14674
14916
|
var DEFAULT_BINARY = "obsidian";
|
|
14675
14917
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
14676
|
-
var SEPARATOR = "\n---\n";
|
|
14677
14918
|
var execFileAsync = promisify(execFile);
|
|
14678
14919
|
var defaultExec = async (binary, args, options) => {
|
|
14679
14920
|
const result = await execFileAsync(binary, args, {
|
|
@@ -14700,8 +14941,10 @@ var ObsidianCLIProvider = class {
|
|
|
14700
14941
|
this.exec = opts.exec ?? defaultExec;
|
|
14701
14942
|
}
|
|
14702
14943
|
async readNote(input) {
|
|
14944
|
+
const path5 = await this.resolvePath(input.identifier);
|
|
14703
14945
|
const { stdout } = await this.runCommand("read", [identifierToArg(input.identifier)]);
|
|
14704
|
-
|
|
14946
|
+
const { frontmatter, content } = splitFrontmatter(stdout);
|
|
14947
|
+
return { path: path5, frontmatter, content };
|
|
14705
14948
|
}
|
|
14706
14949
|
async createNote(input) {
|
|
14707
14950
|
if (input.name === void 0 && input.path === void 0) {
|
|
@@ -14723,8 +14966,11 @@ var ObsidianCLIProvider = class {
|
|
|
14723
14966
|
]);
|
|
14724
14967
|
}
|
|
14725
14968
|
async readDaily() {
|
|
14969
|
+
const { stdout: pathStdout } = await this.runCommand("daily:path", []);
|
|
14970
|
+
const path5 = pathStdout.trim();
|
|
14726
14971
|
const { stdout } = await this.runCommand("daily:read", []);
|
|
14727
|
-
|
|
14972
|
+
const { frontmatter, content } = splitFrontmatter(stdout);
|
|
14973
|
+
return { path: path5, frontmatter, content };
|
|
14728
14974
|
}
|
|
14729
14975
|
async appendDaily(input) {
|
|
14730
14976
|
await this.runCommand("daily:append", [`content=${input.content}`]);
|
|
@@ -14905,15 +15151,21 @@ var ObsidianCLIProvider = class {
|
|
|
14905
15151
|
if (this.vaultName) args.push(`vault=${this.vaultName}`);
|
|
14906
15152
|
return args;
|
|
14907
15153
|
}
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
15154
|
+
async resolvePath(identifier) {
|
|
15155
|
+
if (identifier.kind === "path") return identifier.value;
|
|
15156
|
+
const { stdout } = await this.runCommand("file", [identifierToArg(identifier)]);
|
|
15157
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
15158
|
+
const idx = line.indexOf(" ");
|
|
15159
|
+
if (idx === -1) continue;
|
|
15160
|
+
if (line.slice(0, idx).trim() === "path") {
|
|
15161
|
+
return line.slice(idx + 1).trim();
|
|
15162
|
+
}
|
|
14912
15163
|
}
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
15164
|
+
throw new ToolHandlerError(
|
|
15165
|
+
"CLI_ERROR",
|
|
15166
|
+
`Could not resolve path for name '${identifier.value}': 'path' not found in obsidian file output`,
|
|
15167
|
+
{ details: { name: identifier.value, stdout: stdout.slice(0, 500) } }
|
|
15168
|
+
);
|
|
14917
15169
|
}
|
|
14918
15170
|
};
|
|
14919
15171
|
|
|
@@ -15168,7 +15420,7 @@ function buildOperationsTools(handlers) {
|
|
|
15168
15420
|
name: "read_note",
|
|
15169
15421
|
spec: {
|
|
15170
15422
|
title: "Read Note",
|
|
15171
|
-
description: "Read a note's contents. Provide either `name` (wikilink-style, resolves like Obsidian) or `path` (vault-relative, exact). Returns `{ path, content }
|
|
15423
|
+
description: "Read a note's contents. Provide either `name` (wikilink-style, resolves like Obsidian) or `path` (vault-relative, exact). Returns `{ path, frontmatter, content }` where `frontmatter` is the parsed YAML object (or `null` if absent/malformed) and `content` is the body without the YAML block.",
|
|
15172
15424
|
inputSchema: readNoteSchema
|
|
15173
15425
|
},
|
|
15174
15426
|
handler: async (args) => invokeTool(() => handlers.readNote(readNoteSchema.parse(args)))
|
|
@@ -15195,7 +15447,7 @@ function buildOperationsTools(handlers) {
|
|
|
15195
15447
|
name: "read_daily",
|
|
15196
15448
|
spec: {
|
|
15197
15449
|
title: "Read Daily",
|
|
15198
|
-
description: "Read today's daily note. Returns `{ path, content }
|
|
15450
|
+
description: "Read today's daily note. Returns `{ path, frontmatter, content }` where `frontmatter` is the parsed YAML object (or `null` if absent/malformed) and `content` is the body without the YAML block. Useful for 'what's on my agenda?' questions.",
|
|
15199
15451
|
inputSchema: readDailySchema
|
|
15200
15452
|
},
|
|
15201
15453
|
handler: async () => invokeTool(() => handlers.readDaily({}))
|
|
@@ -15313,10 +15565,9 @@ Use \`search_notes\` when the user is recalling a topic fuzzily, asking a concep
|
|
|
15313
15565
|
|
|
15314
15566
|
### 1. Write the query
|
|
15315
15567
|
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.
|
|
15568
|
+
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.
|
|
15569
|
+
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.
|
|
15570
|
+
4. If a search returns no results, lower the threshold to 0.3 before giving up.
|
|
15320
15571
|
|
|
15321
15572
|
### 2. Choose mode
|
|
15322
15573
|
- **quick** (default) \u2014 returns up to 3 notes, no expansion. Use for specific lookups.
|
|
@@ -15325,6 +15576,8 @@ Use \`search_notes\` when the user is recalling a topic fuzzily, asking a concep
|
|
|
15325
15576
|
|
|
15326
15577
|
### 3. Use the results
|
|
15327
15578
|
- \`results\` \u2014 notes ranked by similarity; read the file by path
|
|
15579
|
+
- \`matched_queries\` (only when \`query\` is an array) \u2014 which of your queries hit this note; lets you spot which synonym was load-bearing
|
|
15580
|
+
- \`truncated\` (only when \`query\` is an array) \u2014 true when more candidates were merged than fit in the cap
|
|
15328
15581
|
- \`blockResults\` \u2014 sections ranked by relevance; use heading + line range to jump to the relevant part
|
|
15329
15582
|
- After finding a relevant note, call get_similar_notes to discover related content
|
|
15330
15583
|
|
|
@@ -15356,6 +15609,7 @@ async function startNeuroVaultServer(config2, deps = {}) {
|
|
|
15356
15609
|
if (config2.semantic.enabled) {
|
|
15357
15610
|
const semantic = await createSemanticModule(
|
|
15358
15611
|
{
|
|
15612
|
+
vaultPath: config2.vaultPath,
|
|
15359
15613
|
smartEnvPath: config2.semantic.smartEnvPath,
|
|
15360
15614
|
modelKey: config2.semantic.modelKey,
|
|
15361
15615
|
modelId: config2.semantic.modelId
|