@velvetmonkey/flywheel-memory 2.0.73 → 2.0.75
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/index.js +288 -55
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1573,7 +1573,7 @@ var init_taskHelpers = __esm({
|
|
|
1573
1573
|
|
|
1574
1574
|
// src/index.ts
|
|
1575
1575
|
import * as path32 from "path";
|
|
1576
|
-
import { readFileSync as
|
|
1576
|
+
import { readFileSync as readFileSync5, realpathSync } from "fs";
|
|
1577
1577
|
import { fileURLToPath } from "url";
|
|
1578
1578
|
import { dirname as dirname4, join as join17 } from "path";
|
|
1579
1579
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -2251,6 +2251,29 @@ function loadEntityEmbeddingsToMemory() {
|
|
|
2251
2251
|
} catch {
|
|
2252
2252
|
}
|
|
2253
2253
|
}
|
|
2254
|
+
function loadNoteEmbeddingsForPaths(paths) {
|
|
2255
|
+
const result = /* @__PURE__ */ new Map();
|
|
2256
|
+
if (!db || paths.length === 0) return result;
|
|
2257
|
+
try {
|
|
2258
|
+
const stmt = db.prepare("SELECT path, embedding FROM note_embeddings WHERE path = ?");
|
|
2259
|
+
for (const p of paths) {
|
|
2260
|
+
const row = stmt.get(p);
|
|
2261
|
+
if (row) {
|
|
2262
|
+
const embedding = new Float32Array(
|
|
2263
|
+
row.embedding.buffer,
|
|
2264
|
+
row.embedding.byteOffset,
|
|
2265
|
+
row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
|
|
2266
|
+
);
|
|
2267
|
+
result.set(p, embedding);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
} catch {
|
|
2271
|
+
}
|
|
2272
|
+
return result;
|
|
2273
|
+
}
|
|
2274
|
+
function getEntityEmbedding(entityName) {
|
|
2275
|
+
return entityEmbeddingsMap.get(entityName) ?? null;
|
|
2276
|
+
}
|
|
2254
2277
|
function getEntityEmbeddingsCount() {
|
|
2255
2278
|
if (!db) return 0;
|
|
2256
2279
|
try {
|
|
@@ -3242,8 +3265,8 @@ async function upsertNote(index, vaultPath2, notePath) {
|
|
|
3242
3265
|
removeNoteFromIndex(index, notePath);
|
|
3243
3266
|
}
|
|
3244
3267
|
const fullPath = path7.join(vaultPath2, notePath);
|
|
3245
|
-
const
|
|
3246
|
-
const stats = await
|
|
3268
|
+
const fs33 = await import("fs/promises");
|
|
3269
|
+
const stats = await fs33.stat(fullPath);
|
|
3247
3270
|
const vaultFile = {
|
|
3248
3271
|
path: notePath,
|
|
3249
3272
|
absolutePath: fullPath,
|
|
@@ -3829,11 +3852,11 @@ function getAllFeedbackBoosts(stateDb2, folder, now) {
|
|
|
3829
3852
|
if (folder !== void 0) {
|
|
3830
3853
|
folderStatsMap = /* @__PURE__ */ new Map();
|
|
3831
3854
|
for (const gs of globalStats) {
|
|
3832
|
-
const
|
|
3833
|
-
if (
|
|
3855
|
+
const fs33 = getWeightedFolderStats(stateDb2, gs.entity, folder, now);
|
|
3856
|
+
if (fs33.rawTotal >= FEEDBACK_BOOST_MIN_SAMPLES) {
|
|
3834
3857
|
folderStatsMap.set(gs.entity, {
|
|
3835
|
-
weightedAccuracy:
|
|
3836
|
-
rawCount:
|
|
3858
|
+
weightedAccuracy: fs33.weightedAccuracy,
|
|
3859
|
+
rawCount: fs33.rawTotal
|
|
3837
3860
|
});
|
|
3838
3861
|
}
|
|
3839
3862
|
}
|
|
@@ -3843,10 +3866,10 @@ function getAllFeedbackBoosts(stateDb2, folder, now) {
|
|
|
3843
3866
|
if (stat4.rawTotal < FEEDBACK_BOOST_MIN_SAMPLES) continue;
|
|
3844
3867
|
let accuracy;
|
|
3845
3868
|
let sampleCount;
|
|
3846
|
-
const
|
|
3847
|
-
if (
|
|
3848
|
-
accuracy =
|
|
3849
|
-
sampleCount =
|
|
3869
|
+
const fs33 = folderStatsMap?.get(stat4.entity);
|
|
3870
|
+
if (fs33 && fs33.rawCount >= FEEDBACK_BOOST_MIN_SAMPLES) {
|
|
3871
|
+
accuracy = fs33.weightedAccuracy;
|
|
3872
|
+
sampleCount = fs33.rawCount;
|
|
3850
3873
|
} else {
|
|
3851
3874
|
accuracy = stat4.weightedAccuracy;
|
|
3852
3875
|
sampleCount = stat4.rawTotal;
|
|
@@ -8490,12 +8513,13 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8490
8513
|
targetMap.set(key, {
|
|
8491
8514
|
count: 1,
|
|
8492
8515
|
sources: /* @__PURE__ */ new Set([broken2.source]),
|
|
8493
|
-
suggestion: broken2.suggestion
|
|
8516
|
+
suggestion: broken2.suggestion,
|
|
8517
|
+
displayTarget: broken2.target
|
|
8494
8518
|
});
|
|
8495
8519
|
}
|
|
8496
8520
|
}
|
|
8497
|
-
const targets = Array.from(targetMap.
|
|
8498
|
-
target,
|
|
8521
|
+
const targets = Array.from(targetMap.values()).map((data) => ({
|
|
8522
|
+
target: data.displayTarget,
|
|
8499
8523
|
mention_count: data.count,
|
|
8500
8524
|
sources: Array.from(data.sources),
|
|
8501
8525
|
...data.suggestion ? { suggestion: data.suggestion } : {}
|
|
@@ -9819,21 +9843,23 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9819
9843
|
if (hasEmbeddingsIndex()) {
|
|
9820
9844
|
try {
|
|
9821
9845
|
const semanticResults = await semanticSearch(query, limit);
|
|
9822
|
-
const
|
|
9823
|
-
const
|
|
9824
|
-
const
|
|
9846
|
+
const normalizePath2 = (p) => p.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
9847
|
+
const fts5Ranked = fts5Results.map((r) => ({ path: normalizePath2(r.path), title: r.title, snippet: r.snippet }));
|
|
9848
|
+
const semanticRanked = semanticResults.map((r) => ({ path: normalizePath2(r.path), title: r.title }));
|
|
9849
|
+
const entityRankedList = entityResults.map((r) => ({ path: normalizePath2(r.path), title: r.name }));
|
|
9850
|
+
const edgeRankedNorm = edgeRanked.map((r) => ({ path: normalizePath2(r.path), title: r.title }));
|
|
9825
9851
|
const rrfLists = [fts5Ranked, semanticRanked, entityRankedList];
|
|
9826
|
-
if (
|
|
9852
|
+
if (edgeRankedNorm.length > 0) rrfLists.push(edgeRankedNorm);
|
|
9827
9853
|
const rrfScores = reciprocalRankFusion(...rrfLists);
|
|
9828
9854
|
const allPaths = /* @__PURE__ */ new Set([
|
|
9829
|
-
...fts5Results.map((r) => r.path),
|
|
9830
|
-
...semanticResults.map((r) => r.path),
|
|
9831
|
-
...entityResults.map((r) => r.path),
|
|
9832
|
-
...edgeRanked.map((r) => r.path)
|
|
9855
|
+
...fts5Results.map((r) => normalizePath2(r.path)),
|
|
9856
|
+
...semanticResults.map((r) => normalizePath2(r.path)),
|
|
9857
|
+
...entityResults.map((r) => normalizePath2(r.path)),
|
|
9858
|
+
...edgeRanked.map((r) => normalizePath2(r.path))
|
|
9833
9859
|
]);
|
|
9834
|
-
const fts5Map = new Map(fts5Results.map((r) => [r.path, r]));
|
|
9835
|
-
const semanticMap = new Map(semanticResults.map((r) => [r.path, r]));
|
|
9836
|
-
const entityMap = new Map(entityResults.map((r) => [r.path, r]));
|
|
9860
|
+
const fts5Map = new Map(fts5Results.map((r) => [normalizePath2(r.path), r]));
|
|
9861
|
+
const semanticMap = new Map(semanticResults.map((r) => [normalizePath2(r.path), r]));
|
|
9862
|
+
const entityMap = new Map(entityResults.map((r) => [normalizePath2(r.path), r]));
|
|
9837
9863
|
const merged = Array.from(allPaths).map((p) => ({
|
|
9838
9864
|
path: p,
|
|
9839
9865
|
title: fts5Map.get(p)?.title || semanticMap.get(p)?.title || entityMap.get(p)?.name || p.replace(/\.md$/, "").split("/").pop() || p,
|
|
@@ -9856,10 +9882,12 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9856
9882
|
}
|
|
9857
9883
|
}
|
|
9858
9884
|
if (entityResults.length > 0) {
|
|
9859
|
-
const
|
|
9860
|
-
const
|
|
9885
|
+
const normalizePath2 = (p) => p.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
9886
|
+
const fts5Map = new Map(fts5Results.map((r) => [normalizePath2(r.path), r]));
|
|
9887
|
+
const entityNormMap = new Map(entityResults.map((r) => [normalizePath2(r.path), r]));
|
|
9888
|
+
const entityRanked = entityResults.filter((r) => !fts5Map.has(normalizePath2(r.path)));
|
|
9861
9889
|
const merged = [
|
|
9862
|
-
...fts5Results.map((r) => ({ path: r.path, title: r.title, snippet: r.snippet, in_entity:
|
|
9890
|
+
...fts5Results.map((r) => ({ path: r.path, title: r.title, snippet: r.snippet, in_entity: entityNormMap.has(normalizePath2(r.path)) })),
|
|
9863
9891
|
...entityRanked.map((r) => ({ path: r.path, title: r.name, snippet: void 0, in_entity: true }))
|
|
9864
9892
|
];
|
|
9865
9893
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
@@ -17363,6 +17391,142 @@ function registerMemoryTools(server2, getStateDb) {
|
|
|
17363
17391
|
// src/tools/read/recall.ts
|
|
17364
17392
|
import { z as z24 } from "zod";
|
|
17365
17393
|
import { searchEntities as searchEntitiesDb2 } from "@velvetmonkey/vault-core";
|
|
17394
|
+
|
|
17395
|
+
// src/core/read/mmr.ts
|
|
17396
|
+
function selectByMmr(candidates, limit, lambda = 0.7) {
|
|
17397
|
+
if (candidates.length <= limit) return candidates;
|
|
17398
|
+
if (candidates.length === 0) return [];
|
|
17399
|
+
const maxScore = Math.max(...candidates.map((c) => c.score));
|
|
17400
|
+
if (maxScore === 0) return candidates.slice(0, limit);
|
|
17401
|
+
const normScores = /* @__PURE__ */ new Map();
|
|
17402
|
+
for (const c of candidates) {
|
|
17403
|
+
normScores.set(c.id, c.score / maxScore);
|
|
17404
|
+
}
|
|
17405
|
+
const selected = [];
|
|
17406
|
+
const remaining = new Set(candidates.map((_, i) => i));
|
|
17407
|
+
let bestIdx = 0;
|
|
17408
|
+
let bestScore = -Infinity;
|
|
17409
|
+
for (const idx of remaining) {
|
|
17410
|
+
if (candidates[idx].score > bestScore) {
|
|
17411
|
+
bestScore = candidates[idx].score;
|
|
17412
|
+
bestIdx = idx;
|
|
17413
|
+
}
|
|
17414
|
+
}
|
|
17415
|
+
selected.push(candidates[bestIdx]);
|
|
17416
|
+
remaining.delete(bestIdx);
|
|
17417
|
+
while (selected.length < limit && remaining.size > 0) {
|
|
17418
|
+
let bestMmr = -Infinity;
|
|
17419
|
+
let bestCandidate = -1;
|
|
17420
|
+
for (const idx of remaining) {
|
|
17421
|
+
const candidate = candidates[idx];
|
|
17422
|
+
const relevance = normScores.get(candidate.id) || 0;
|
|
17423
|
+
let maxSim = 0;
|
|
17424
|
+
if (candidate.embedding !== null) {
|
|
17425
|
+
for (const sel of selected) {
|
|
17426
|
+
if (sel.embedding !== null) {
|
|
17427
|
+
const sim = cosineSimilarity(candidate.embedding, sel.embedding);
|
|
17428
|
+
if (sim > maxSim) maxSim = sim;
|
|
17429
|
+
}
|
|
17430
|
+
}
|
|
17431
|
+
}
|
|
17432
|
+
const mmr = lambda * relevance - (1 - lambda) * maxSim;
|
|
17433
|
+
if (mmr > bestMmr) {
|
|
17434
|
+
bestMmr = mmr;
|
|
17435
|
+
bestCandidate = idx;
|
|
17436
|
+
}
|
|
17437
|
+
}
|
|
17438
|
+
if (bestCandidate === -1) break;
|
|
17439
|
+
selected.push(candidates[bestCandidate]);
|
|
17440
|
+
remaining.delete(bestCandidate);
|
|
17441
|
+
}
|
|
17442
|
+
return selected;
|
|
17443
|
+
}
|
|
17444
|
+
|
|
17445
|
+
// src/core/read/snippets.ts
|
|
17446
|
+
import * as fs29 from "fs";
|
|
17447
|
+
function stripFrontmatter(content) {
|
|
17448
|
+
const match = content.match(/^---[\s\S]*?---\n([\s\S]*)$/);
|
|
17449
|
+
return match ? match[1] : content;
|
|
17450
|
+
}
|
|
17451
|
+
function splitIntoParagraphs(content, maxChunkChars) {
|
|
17452
|
+
const MIN_PARAGRAPH_CHARS = 50;
|
|
17453
|
+
const raw = content.split(/\n\n+/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
17454
|
+
const merged = [];
|
|
17455
|
+
let buffer2 = "";
|
|
17456
|
+
for (const paragraph of raw) {
|
|
17457
|
+
if (buffer2) {
|
|
17458
|
+
buffer2 += "\n\n" + paragraph;
|
|
17459
|
+
if (buffer2.length >= MIN_PARAGRAPH_CHARS) {
|
|
17460
|
+
merged.push(buffer2.slice(0, maxChunkChars));
|
|
17461
|
+
buffer2 = "";
|
|
17462
|
+
}
|
|
17463
|
+
} else if (paragraph.length < MIN_PARAGRAPH_CHARS) {
|
|
17464
|
+
buffer2 = paragraph;
|
|
17465
|
+
} else {
|
|
17466
|
+
merged.push(paragraph.slice(0, maxChunkChars));
|
|
17467
|
+
}
|
|
17468
|
+
}
|
|
17469
|
+
if (buffer2) {
|
|
17470
|
+
merged.push(buffer2.slice(0, maxChunkChars));
|
|
17471
|
+
}
|
|
17472
|
+
return merged;
|
|
17473
|
+
}
|
|
17474
|
+
function scoreByKeywords(chunk, queryTokens, queryStems) {
|
|
17475
|
+
const chunkTokens = new Set(tokenize(chunk.toLowerCase()));
|
|
17476
|
+
const chunkStems = new Set([...chunkTokens].map((t) => stem(t)));
|
|
17477
|
+
let score = 0;
|
|
17478
|
+
for (let i = 0; i < queryTokens.length; i++) {
|
|
17479
|
+
if (chunkTokens.has(queryTokens[i])) {
|
|
17480
|
+
score += 10;
|
|
17481
|
+
} else if (chunkStems.has(queryStems[i])) {
|
|
17482
|
+
score += 5;
|
|
17483
|
+
}
|
|
17484
|
+
}
|
|
17485
|
+
return score;
|
|
17486
|
+
}
|
|
17487
|
+
async function extractBestSnippets(filePath, queryEmbedding, queryTokens, options) {
|
|
17488
|
+
const maxSnippets = options?.maxSnippets ?? 1;
|
|
17489
|
+
const maxChunkChars = options?.maxChunkChars ?? 500;
|
|
17490
|
+
let content;
|
|
17491
|
+
try {
|
|
17492
|
+
content = fs29.readFileSync(filePath, "utf-8");
|
|
17493
|
+
} catch {
|
|
17494
|
+
return [];
|
|
17495
|
+
}
|
|
17496
|
+
const body = stripFrontmatter(content);
|
|
17497
|
+
if (body.length < 50) {
|
|
17498
|
+
return body.length > 0 ? [{ text: body, score: 1 }] : [];
|
|
17499
|
+
}
|
|
17500
|
+
const paragraphs = splitIntoParagraphs(body, maxChunkChars);
|
|
17501
|
+
if (paragraphs.length === 0) return [];
|
|
17502
|
+
const queryStems = queryTokens.map((t) => stem(t));
|
|
17503
|
+
const scored = paragraphs.map((text, idx) => ({
|
|
17504
|
+
text,
|
|
17505
|
+
idx,
|
|
17506
|
+
keywordScore: scoreByKeywords(text, queryTokens, queryStems)
|
|
17507
|
+
}));
|
|
17508
|
+
scored.sort((a, b) => b.keywordScore - a.keywordScore);
|
|
17509
|
+
const topKeyword = scored.slice(0, 5);
|
|
17510
|
+
if (queryEmbedding && hasEmbeddingsIndex()) {
|
|
17511
|
+
try {
|
|
17512
|
+
const reranked = [];
|
|
17513
|
+
for (const chunk of topKeyword) {
|
|
17514
|
+
const chunkEmbedding = await embedTextCached(chunk.text);
|
|
17515
|
+
const sim = cosineSimilarity(queryEmbedding, chunkEmbedding);
|
|
17516
|
+
reranked.push({ text: chunk.text, score: sim });
|
|
17517
|
+
}
|
|
17518
|
+
reranked.sort((a, b) => b.score - a.score);
|
|
17519
|
+
return reranked.slice(0, maxSnippets);
|
|
17520
|
+
} catch {
|
|
17521
|
+
}
|
|
17522
|
+
}
|
|
17523
|
+
return topKeyword.slice(0, maxSnippets).map((c) => ({
|
|
17524
|
+
text: c.text,
|
|
17525
|
+
score: c.keywordScore
|
|
17526
|
+
}));
|
|
17527
|
+
}
|
|
17528
|
+
|
|
17529
|
+
// src/tools/read/recall.ts
|
|
17366
17530
|
function scoreTextRelevance(query, content) {
|
|
17367
17531
|
const queryTokens = tokenize(query).map((t) => t.toLowerCase());
|
|
17368
17532
|
const queryStems = queryTokens.map((t) => stem(t));
|
|
@@ -17394,7 +17558,9 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
17394
17558
|
max_results = 20,
|
|
17395
17559
|
focus = "all",
|
|
17396
17560
|
entity,
|
|
17397
|
-
max_tokens
|
|
17561
|
+
max_tokens,
|
|
17562
|
+
diversity = 0.7,
|
|
17563
|
+
vaultPath: vaultPath2
|
|
17398
17564
|
} = options;
|
|
17399
17565
|
const results = [];
|
|
17400
17566
|
const recencyIndex2 = loadRecencyFromStateDb();
|
|
@@ -17519,11 +17685,60 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
17519
17685
|
seen.add(key);
|
|
17520
17686
|
return true;
|
|
17521
17687
|
});
|
|
17522
|
-
|
|
17688
|
+
let selected;
|
|
17689
|
+
if (hasEmbeddingsIndex() && deduped.length > max_results) {
|
|
17690
|
+
const notePaths = deduped.filter((r) => r.type === "note").map((r) => r.id);
|
|
17691
|
+
const noteEmbeddings = loadNoteEmbeddingsForPaths(notePaths);
|
|
17692
|
+
let queryEmbedding = null;
|
|
17693
|
+
const mmrCandidates = [];
|
|
17694
|
+
for (const r of deduped) {
|
|
17695
|
+
let embedding = null;
|
|
17696
|
+
if (r.type === "entity") {
|
|
17697
|
+
embedding = getEntityEmbedding(r.id);
|
|
17698
|
+
} else if (r.type === "note") {
|
|
17699
|
+
embedding = noteEmbeddings.get(r.id) ?? null;
|
|
17700
|
+
} else if (r.type === "memory") {
|
|
17701
|
+
try {
|
|
17702
|
+
if (!queryEmbedding) queryEmbedding = await embedTextCached(query);
|
|
17703
|
+
embedding = await embedTextCached(r.content);
|
|
17704
|
+
} catch {
|
|
17705
|
+
}
|
|
17706
|
+
}
|
|
17707
|
+
mmrCandidates.push({ id: `${r.type}:${r.id}`, score: r.score, embedding });
|
|
17708
|
+
}
|
|
17709
|
+
const mmrSelected = selectByMmr(mmrCandidates, max_results, diversity);
|
|
17710
|
+
const selectedIds = new Set(mmrSelected.map((m) => m.id));
|
|
17711
|
+
selected = deduped.filter((r) => selectedIds.has(`${r.type}:${r.id}`));
|
|
17712
|
+
const orderMap = new Map(mmrSelected.map((m, i) => [m.id, i]));
|
|
17713
|
+
selected.sort((a, b) => (orderMap.get(`${a.type}:${a.id}`) ?? 0) - (orderMap.get(`${b.type}:${b.id}`) ?? 0));
|
|
17714
|
+
} else {
|
|
17715
|
+
selected = deduped.slice(0, max_results);
|
|
17716
|
+
}
|
|
17717
|
+
if (vaultPath2) {
|
|
17718
|
+
const queryTokens = tokenize(query).map((t) => t.toLowerCase());
|
|
17719
|
+
let queryEmb = null;
|
|
17720
|
+
if (hasEmbeddingsIndex()) {
|
|
17721
|
+
try {
|
|
17722
|
+
queryEmb = await embedTextCached(query);
|
|
17723
|
+
} catch {
|
|
17724
|
+
}
|
|
17725
|
+
}
|
|
17726
|
+
for (const r of selected) {
|
|
17727
|
+
if (r.type !== "note") continue;
|
|
17728
|
+
try {
|
|
17729
|
+
const absPath = vaultPath2 + "/" + r.id;
|
|
17730
|
+
const snippets = await extractBestSnippets(absPath, queryEmb, queryTokens);
|
|
17731
|
+
if (snippets.length > 0 && snippets[0].text.length > 0) {
|
|
17732
|
+
r.content = snippets[0].text;
|
|
17733
|
+
}
|
|
17734
|
+
} catch {
|
|
17735
|
+
}
|
|
17736
|
+
}
|
|
17737
|
+
}
|
|
17523
17738
|
if (max_tokens) {
|
|
17524
17739
|
let tokenBudget = max_tokens;
|
|
17525
17740
|
const budgeted = [];
|
|
17526
|
-
for (const r of
|
|
17741
|
+
for (const r of selected) {
|
|
17527
17742
|
const estimatedTokens = Math.ceil(r.content.length / 4);
|
|
17528
17743
|
if (tokenBudget - estimatedTokens < 0 && budgeted.length > 0) break;
|
|
17529
17744
|
tokenBudget -= estimatedTokens;
|
|
@@ -17531,9 +17746,9 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
17531
17746
|
}
|
|
17532
17747
|
return budgeted;
|
|
17533
17748
|
}
|
|
17534
|
-
return
|
|
17749
|
+
return selected;
|
|
17535
17750
|
}
|
|
17536
|
-
function registerRecallTools(server2, getStateDb) {
|
|
17751
|
+
function registerRecallTools(server2, getStateDb, getVaultPath) {
|
|
17537
17752
|
server2.tool(
|
|
17538
17753
|
"recall",
|
|
17539
17754
|
"Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
|
|
@@ -17542,7 +17757,8 @@ function registerRecallTools(server2, getStateDb) {
|
|
|
17542
17757
|
max_results: z24.number().min(1).max(100).optional().describe("Max results (default: 20)"),
|
|
17543
17758
|
focus: z24.enum(["entities", "notes", "memories", "all"]).optional().describe("Limit search to specific type (default: all)"),
|
|
17544
17759
|
entity: z24.string().optional().describe("Filter memories by entity association"),
|
|
17545
|
-
max_tokens: z24.number().optional().describe("Token budget for response (truncates lower-ranked results)")
|
|
17760
|
+
max_tokens: z24.number().optional().describe("Token budget for response (truncates lower-ranked results)"),
|
|
17761
|
+
diversity: z24.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
|
|
17546
17762
|
},
|
|
17547
17763
|
async (args) => {
|
|
17548
17764
|
const stateDb2 = getStateDb();
|
|
@@ -17556,7 +17772,9 @@ function registerRecallTools(server2, getStateDb) {
|
|
|
17556
17772
|
max_results: args.max_results,
|
|
17557
17773
|
focus: args.focus,
|
|
17558
17774
|
entity: args.entity,
|
|
17559
|
-
max_tokens: args.max_tokens
|
|
17775
|
+
max_tokens: args.max_tokens,
|
|
17776
|
+
diversity: args.diversity,
|
|
17777
|
+
vaultPath: getVaultPath?.()
|
|
17560
17778
|
});
|
|
17561
17779
|
const entities = results.filter((r) => r.type === "entity");
|
|
17562
17780
|
const notes = results.filter((r) => r.type === "note");
|
|
@@ -17992,7 +18210,7 @@ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
|
17992
18210
|
|
|
17993
18211
|
// src/tools/write/enrich.ts
|
|
17994
18212
|
import { z as z27 } from "zod";
|
|
17995
|
-
import * as
|
|
18213
|
+
import * as fs30 from "fs/promises";
|
|
17996
18214
|
import * as path30 from "path";
|
|
17997
18215
|
function hasSkipWikilinks(content) {
|
|
17998
18216
|
if (!content.startsWith("---")) return false;
|
|
@@ -18004,7 +18222,7 @@ function hasSkipWikilinks(content) {
|
|
|
18004
18222
|
async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
|
|
18005
18223
|
const results = [];
|
|
18006
18224
|
try {
|
|
18007
|
-
const entries = await
|
|
18225
|
+
const entries = await fs30.readdir(dirPath, { withFileTypes: true });
|
|
18008
18226
|
for (const entry of entries) {
|
|
18009
18227
|
if (entry.name.startsWith(".")) continue;
|
|
18010
18228
|
const fullPath = path30.join(dirPath, entry.name);
|
|
@@ -18079,7 +18297,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
18079
18297
|
const fullPath = path30.join(vaultPath2, relativePath);
|
|
18080
18298
|
let content;
|
|
18081
18299
|
try {
|
|
18082
|
-
content = await
|
|
18300
|
+
content = await fs30.readFile(fullPath, "utf-8");
|
|
18083
18301
|
} catch {
|
|
18084
18302
|
continue;
|
|
18085
18303
|
}
|
|
@@ -18107,7 +18325,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
18107
18325
|
});
|
|
18108
18326
|
if (!dry_run) {
|
|
18109
18327
|
const fullPath = path30.join(vaultPath2, relativePath);
|
|
18110
|
-
await
|
|
18328
|
+
await fs30.writeFile(fullPath, result.content, "utf-8");
|
|
18111
18329
|
notesModified++;
|
|
18112
18330
|
if (stateDb2) {
|
|
18113
18331
|
trackWikilinkApplications(stateDb2, relativePath, entities);
|
|
@@ -18466,7 +18684,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
18466
18684
|
import { z as z30 } from "zod";
|
|
18467
18685
|
|
|
18468
18686
|
// src/core/read/similarity.ts
|
|
18469
|
-
import * as
|
|
18687
|
+
import * as fs31 from "fs";
|
|
18470
18688
|
import * as path31 from "path";
|
|
18471
18689
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
18472
18690
|
"the",
|
|
@@ -18607,7 +18825,7 @@ function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
|
18607
18825
|
const absPath = path31.join(vaultPath2, sourcePath);
|
|
18608
18826
|
let content;
|
|
18609
18827
|
try {
|
|
18610
|
-
content =
|
|
18828
|
+
content = fs31.readFileSync(absPath, "utf-8");
|
|
18611
18829
|
} catch {
|
|
18612
18830
|
return [];
|
|
18613
18831
|
}
|
|
@@ -18685,6 +18903,7 @@ async function findSemanticSimilarNotes(vaultPath2, index, sourcePath, options =
|
|
|
18685
18903
|
}
|
|
18686
18904
|
async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
18687
18905
|
const limit = options.limit ?? 10;
|
|
18906
|
+
const diversity = options.diversity ?? 0.7;
|
|
18688
18907
|
const bm25Results = findSimilarNotes(db4, vaultPath2, index, sourcePath, {
|
|
18689
18908
|
limit: limit * 2,
|
|
18690
18909
|
excludeLinked: options.excludeLinked
|
|
@@ -18716,7 +18935,19 @@ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, option
|
|
|
18716
18935
|
};
|
|
18717
18936
|
});
|
|
18718
18937
|
merged.sort((a, b) => b.score - a.score);
|
|
18719
|
-
|
|
18938
|
+
if (merged.length > limit) {
|
|
18939
|
+
const noteEmbeddings = loadNoteEmbeddingsForPaths(merged.map((m) => m.path));
|
|
18940
|
+
const mmrCandidates = merged.map((m) => ({
|
|
18941
|
+
id: m.path,
|
|
18942
|
+
score: m.score,
|
|
18943
|
+
embedding: noteEmbeddings.get(m.path) ?? null
|
|
18944
|
+
}));
|
|
18945
|
+
const mmrSelected = selectByMmr(mmrCandidates, limit, diversity);
|
|
18946
|
+
const selectedPaths = new Set(mmrSelected.map((m) => m.id));
|
|
18947
|
+
const mergedMap = new Map(merged.map((m) => [m.path, m]));
|
|
18948
|
+
return mmrSelected.map((m) => mergedMap.get(m.id)).filter(Boolean);
|
|
18949
|
+
}
|
|
18950
|
+
return merged;
|
|
18720
18951
|
}
|
|
18721
18952
|
|
|
18722
18953
|
// src/tools/read/similarity.ts
|
|
@@ -18729,10 +18960,11 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
18729
18960
|
inputSchema: {
|
|
18730
18961
|
path: z30.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
|
|
18731
18962
|
limit: z30.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
|
|
18732
|
-
exclude_linked: z30.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
|
|
18963
|
+
exclude_linked: z30.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)"),
|
|
18964
|
+
diversity: z30.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
|
|
18733
18965
|
}
|
|
18734
18966
|
},
|
|
18735
|
-
async ({ path: path33, limit, exclude_linked }) => {
|
|
18967
|
+
async ({ path: path33, limit, exclude_linked, diversity }) => {
|
|
18736
18968
|
const index = getIndex();
|
|
18737
18969
|
const vaultPath2 = getVaultPath();
|
|
18738
18970
|
const stateDb2 = getStateDb();
|
|
@@ -18751,7 +18983,8 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
18751
18983
|
}
|
|
18752
18984
|
const opts = {
|
|
18753
18985
|
limit: limit ?? 10,
|
|
18754
|
-
excludeLinked: exclude_linked ?? true
|
|
18986
|
+
excludeLinked: exclude_linked ?? true,
|
|
18987
|
+
diversity: diversity ?? 0.7
|
|
18755
18988
|
};
|
|
18756
18989
|
const useHybrid = hasEmbeddingsIndex();
|
|
18757
18990
|
const method = useHybrid ? "hybrid" : "bm25";
|
|
@@ -18997,7 +19230,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
18997
19230
|
}
|
|
18998
19231
|
|
|
18999
19232
|
// src/index.ts
|
|
19000
|
-
import * as
|
|
19233
|
+
import * as fs32 from "node:fs/promises";
|
|
19001
19234
|
import { createHash as createHash3 } from "node:crypto";
|
|
19002
19235
|
|
|
19003
19236
|
// src/resources/vault.ts
|
|
@@ -19106,7 +19339,7 @@ function registerVaultResources(server2, getIndex) {
|
|
|
19106
19339
|
// src/index.ts
|
|
19107
19340
|
var __filename = fileURLToPath(import.meta.url);
|
|
19108
19341
|
var __dirname = dirname4(__filename);
|
|
19109
|
-
var pkg = JSON.parse(
|
|
19342
|
+
var pkg = JSON.parse(readFileSync5(join17(__dirname, "../package.json"), "utf-8"));
|
|
19110
19343
|
var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
19111
19344
|
var resolvedVaultPath;
|
|
19112
19345
|
try {
|
|
@@ -19413,7 +19646,7 @@ registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb
|
|
|
19413
19646
|
registerSemanticTools(server, () => vaultPath, () => stateDb);
|
|
19414
19647
|
registerMergeTools2(server, () => stateDb);
|
|
19415
19648
|
registerMemoryTools(server, () => stateDb);
|
|
19416
|
-
registerRecallTools(server, () => stateDb);
|
|
19649
|
+
registerRecallTools(server, () => stateDb, () => vaultPath);
|
|
19417
19650
|
registerBriefTools(server, () => stateDb);
|
|
19418
19651
|
registerVaultResources(server, () => vaultIndex ?? null);
|
|
19419
19652
|
serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
@@ -19559,7 +19792,7 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
|
19559
19792
|
async function scanDir(dir) {
|
|
19560
19793
|
let entries;
|
|
19561
19794
|
try {
|
|
19562
|
-
entries = await
|
|
19795
|
+
entries = await fs32.readdir(dir, { withFileTypes: true });
|
|
19563
19796
|
} catch {
|
|
19564
19797
|
return;
|
|
19565
19798
|
}
|
|
@@ -19570,7 +19803,7 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
|
19570
19803
|
await scanDir(fullPath);
|
|
19571
19804
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
19572
19805
|
try {
|
|
19573
|
-
const stat4 = await
|
|
19806
|
+
const stat4 = await fs32.stat(fullPath);
|
|
19574
19807
|
if (stat4.mtimeMs > sinceMs) {
|
|
19575
19808
|
events.push({
|
|
19576
19809
|
type: "upsert",
|
|
@@ -19773,7 +20006,7 @@ async function runPostIndexWork(index) {
|
|
|
19773
20006
|
continue;
|
|
19774
20007
|
}
|
|
19775
20008
|
try {
|
|
19776
|
-
const content = await
|
|
20009
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
19777
20010
|
const hash = createHash3("sha256").update(content).digest("hex").slice(0, 16);
|
|
19778
20011
|
if (lastContentHashes.get(event.path) === hash) {
|
|
19779
20012
|
serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
|
|
@@ -20211,7 +20444,7 @@ async function runPostIndexWork(index) {
|
|
|
20211
20444
|
for (const event of filteredEvents) {
|
|
20212
20445
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20213
20446
|
try {
|
|
20214
|
-
const content = await
|
|
20447
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20215
20448
|
const zones = getProtectedZones2(content);
|
|
20216
20449
|
const linked = new Set(
|
|
20217
20450
|
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).map((n) => n.toLowerCase())
|
|
@@ -20252,7 +20485,7 @@ async function runPostIndexWork(index) {
|
|
|
20252
20485
|
for (const event of filteredEvents) {
|
|
20253
20486
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20254
20487
|
try {
|
|
20255
|
-
const content = await
|
|
20488
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20256
20489
|
const removed = processImplicitFeedback(stateDb, event.path, content);
|
|
20257
20490
|
for (const entity of removed) feedbackResults.push({ entity, file: event.path });
|
|
20258
20491
|
} catch {
|
|
@@ -20332,7 +20565,7 @@ async function runPostIndexWork(index) {
|
|
|
20332
20565
|
for (const event of filteredEvents) {
|
|
20333
20566
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20334
20567
|
try {
|
|
20335
|
-
const content = await
|
|
20568
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20336
20569
|
const zones = getProtectedZones2(content);
|
|
20337
20570
|
const linkedSet = new Set(
|
|
20338
20571
|
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).concat(forwardLinkResults.find((r) => r.file === event.path)?.dead ?? []).map((n) => n.toLowerCase())
|
|
@@ -20365,7 +20598,7 @@ async function runPostIndexWork(index) {
|
|
|
20365
20598
|
for (const event of filteredEvents) {
|
|
20366
20599
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20367
20600
|
try {
|
|
20368
|
-
const content = await
|
|
20601
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20369
20602
|
const result = await suggestRelatedLinks(content, {
|
|
20370
20603
|
maxSuggestions: 5,
|
|
20371
20604
|
strictness: "balanced",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.75",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 51 tools for search, backlinks, graph queries, mutations, agent memory, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
55
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
55
|
+
"@velvetmonkey/vault-core": "^2.0.75",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|