@velvetmonkey/flywheel-memory 2.0.73 → 2.0.74
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 +270 -41
- 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 } : {}
|
|
@@ -17363,6 +17387,142 @@ function registerMemoryTools(server2, getStateDb) {
|
|
|
17363
17387
|
// src/tools/read/recall.ts
|
|
17364
17388
|
import { z as z24 } from "zod";
|
|
17365
17389
|
import { searchEntities as searchEntitiesDb2 } from "@velvetmonkey/vault-core";
|
|
17390
|
+
|
|
17391
|
+
// src/core/read/mmr.ts
|
|
17392
|
+
function selectByMmr(candidates, limit, lambda = 0.7) {
|
|
17393
|
+
if (candidates.length <= limit) return candidates;
|
|
17394
|
+
if (candidates.length === 0) return [];
|
|
17395
|
+
const maxScore = Math.max(...candidates.map((c) => c.score));
|
|
17396
|
+
if (maxScore === 0) return candidates.slice(0, limit);
|
|
17397
|
+
const normScores = /* @__PURE__ */ new Map();
|
|
17398
|
+
for (const c of candidates) {
|
|
17399
|
+
normScores.set(c.id, c.score / maxScore);
|
|
17400
|
+
}
|
|
17401
|
+
const selected = [];
|
|
17402
|
+
const remaining = new Set(candidates.map((_, i) => i));
|
|
17403
|
+
let bestIdx = 0;
|
|
17404
|
+
let bestScore = -Infinity;
|
|
17405
|
+
for (const idx of remaining) {
|
|
17406
|
+
if (candidates[idx].score > bestScore) {
|
|
17407
|
+
bestScore = candidates[idx].score;
|
|
17408
|
+
bestIdx = idx;
|
|
17409
|
+
}
|
|
17410
|
+
}
|
|
17411
|
+
selected.push(candidates[bestIdx]);
|
|
17412
|
+
remaining.delete(bestIdx);
|
|
17413
|
+
while (selected.length < limit && remaining.size > 0) {
|
|
17414
|
+
let bestMmr = -Infinity;
|
|
17415
|
+
let bestCandidate = -1;
|
|
17416
|
+
for (const idx of remaining) {
|
|
17417
|
+
const candidate = candidates[idx];
|
|
17418
|
+
const relevance = normScores.get(candidate.id) || 0;
|
|
17419
|
+
let maxSim = 0;
|
|
17420
|
+
if (candidate.embedding !== null) {
|
|
17421
|
+
for (const sel of selected) {
|
|
17422
|
+
if (sel.embedding !== null) {
|
|
17423
|
+
const sim = cosineSimilarity(candidate.embedding, sel.embedding);
|
|
17424
|
+
if (sim > maxSim) maxSim = sim;
|
|
17425
|
+
}
|
|
17426
|
+
}
|
|
17427
|
+
}
|
|
17428
|
+
const mmr = lambda * relevance - (1 - lambda) * maxSim;
|
|
17429
|
+
if (mmr > bestMmr) {
|
|
17430
|
+
bestMmr = mmr;
|
|
17431
|
+
bestCandidate = idx;
|
|
17432
|
+
}
|
|
17433
|
+
}
|
|
17434
|
+
if (bestCandidate === -1) break;
|
|
17435
|
+
selected.push(candidates[bestCandidate]);
|
|
17436
|
+
remaining.delete(bestCandidate);
|
|
17437
|
+
}
|
|
17438
|
+
return selected;
|
|
17439
|
+
}
|
|
17440
|
+
|
|
17441
|
+
// src/core/read/snippets.ts
|
|
17442
|
+
import * as fs29 from "fs";
|
|
17443
|
+
function stripFrontmatter(content) {
|
|
17444
|
+
const match = content.match(/^---[\s\S]*?---\n([\s\S]*)$/);
|
|
17445
|
+
return match ? match[1] : content;
|
|
17446
|
+
}
|
|
17447
|
+
function splitIntoParagraphs(content, maxChunkChars) {
|
|
17448
|
+
const MIN_PARAGRAPH_CHARS = 50;
|
|
17449
|
+
const raw = content.split(/\n\n+/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
17450
|
+
const merged = [];
|
|
17451
|
+
let buffer2 = "";
|
|
17452
|
+
for (const paragraph of raw) {
|
|
17453
|
+
if (buffer2) {
|
|
17454
|
+
buffer2 += "\n\n" + paragraph;
|
|
17455
|
+
if (buffer2.length >= MIN_PARAGRAPH_CHARS) {
|
|
17456
|
+
merged.push(buffer2.slice(0, maxChunkChars));
|
|
17457
|
+
buffer2 = "";
|
|
17458
|
+
}
|
|
17459
|
+
} else if (paragraph.length < MIN_PARAGRAPH_CHARS) {
|
|
17460
|
+
buffer2 = paragraph;
|
|
17461
|
+
} else {
|
|
17462
|
+
merged.push(paragraph.slice(0, maxChunkChars));
|
|
17463
|
+
}
|
|
17464
|
+
}
|
|
17465
|
+
if (buffer2) {
|
|
17466
|
+
merged.push(buffer2.slice(0, maxChunkChars));
|
|
17467
|
+
}
|
|
17468
|
+
return merged;
|
|
17469
|
+
}
|
|
17470
|
+
function scoreByKeywords(chunk, queryTokens, queryStems) {
|
|
17471
|
+
const chunkTokens = new Set(tokenize(chunk.toLowerCase()));
|
|
17472
|
+
const chunkStems = new Set([...chunkTokens].map((t) => stem(t)));
|
|
17473
|
+
let score = 0;
|
|
17474
|
+
for (let i = 0; i < queryTokens.length; i++) {
|
|
17475
|
+
if (chunkTokens.has(queryTokens[i])) {
|
|
17476
|
+
score += 10;
|
|
17477
|
+
} else if (chunkStems.has(queryStems[i])) {
|
|
17478
|
+
score += 5;
|
|
17479
|
+
}
|
|
17480
|
+
}
|
|
17481
|
+
return score;
|
|
17482
|
+
}
|
|
17483
|
+
async function extractBestSnippets(filePath, queryEmbedding, queryTokens, options) {
|
|
17484
|
+
const maxSnippets = options?.maxSnippets ?? 1;
|
|
17485
|
+
const maxChunkChars = options?.maxChunkChars ?? 500;
|
|
17486
|
+
let content;
|
|
17487
|
+
try {
|
|
17488
|
+
content = fs29.readFileSync(filePath, "utf-8");
|
|
17489
|
+
} catch {
|
|
17490
|
+
return [];
|
|
17491
|
+
}
|
|
17492
|
+
const body = stripFrontmatter(content);
|
|
17493
|
+
if (body.length < 50) {
|
|
17494
|
+
return body.length > 0 ? [{ text: body, score: 1 }] : [];
|
|
17495
|
+
}
|
|
17496
|
+
const paragraphs = splitIntoParagraphs(body, maxChunkChars);
|
|
17497
|
+
if (paragraphs.length === 0) return [];
|
|
17498
|
+
const queryStems = queryTokens.map((t) => stem(t));
|
|
17499
|
+
const scored = paragraphs.map((text, idx) => ({
|
|
17500
|
+
text,
|
|
17501
|
+
idx,
|
|
17502
|
+
keywordScore: scoreByKeywords(text, queryTokens, queryStems)
|
|
17503
|
+
}));
|
|
17504
|
+
scored.sort((a, b) => b.keywordScore - a.keywordScore);
|
|
17505
|
+
const topKeyword = scored.slice(0, 5);
|
|
17506
|
+
if (queryEmbedding && hasEmbeddingsIndex()) {
|
|
17507
|
+
try {
|
|
17508
|
+
const reranked = [];
|
|
17509
|
+
for (const chunk of topKeyword) {
|
|
17510
|
+
const chunkEmbedding = await embedTextCached(chunk.text);
|
|
17511
|
+
const sim = cosineSimilarity(queryEmbedding, chunkEmbedding);
|
|
17512
|
+
reranked.push({ text: chunk.text, score: sim });
|
|
17513
|
+
}
|
|
17514
|
+
reranked.sort((a, b) => b.score - a.score);
|
|
17515
|
+
return reranked.slice(0, maxSnippets);
|
|
17516
|
+
} catch {
|
|
17517
|
+
}
|
|
17518
|
+
}
|
|
17519
|
+
return topKeyword.slice(0, maxSnippets).map((c) => ({
|
|
17520
|
+
text: c.text,
|
|
17521
|
+
score: c.keywordScore
|
|
17522
|
+
}));
|
|
17523
|
+
}
|
|
17524
|
+
|
|
17525
|
+
// src/tools/read/recall.ts
|
|
17366
17526
|
function scoreTextRelevance(query, content) {
|
|
17367
17527
|
const queryTokens = tokenize(query).map((t) => t.toLowerCase());
|
|
17368
17528
|
const queryStems = queryTokens.map((t) => stem(t));
|
|
@@ -17394,7 +17554,9 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
17394
17554
|
max_results = 20,
|
|
17395
17555
|
focus = "all",
|
|
17396
17556
|
entity,
|
|
17397
|
-
max_tokens
|
|
17557
|
+
max_tokens,
|
|
17558
|
+
diversity = 0.7,
|
|
17559
|
+
vaultPath: vaultPath2
|
|
17398
17560
|
} = options;
|
|
17399
17561
|
const results = [];
|
|
17400
17562
|
const recencyIndex2 = loadRecencyFromStateDb();
|
|
@@ -17519,11 +17681,60 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
17519
17681
|
seen.add(key);
|
|
17520
17682
|
return true;
|
|
17521
17683
|
});
|
|
17522
|
-
|
|
17684
|
+
let selected;
|
|
17685
|
+
if (hasEmbeddingsIndex() && deduped.length > max_results) {
|
|
17686
|
+
const notePaths = deduped.filter((r) => r.type === "note").map((r) => r.id);
|
|
17687
|
+
const noteEmbeddings = loadNoteEmbeddingsForPaths(notePaths);
|
|
17688
|
+
let queryEmbedding = null;
|
|
17689
|
+
const mmrCandidates = [];
|
|
17690
|
+
for (const r of deduped) {
|
|
17691
|
+
let embedding = null;
|
|
17692
|
+
if (r.type === "entity") {
|
|
17693
|
+
embedding = getEntityEmbedding(r.id);
|
|
17694
|
+
} else if (r.type === "note") {
|
|
17695
|
+
embedding = noteEmbeddings.get(r.id) ?? null;
|
|
17696
|
+
} else if (r.type === "memory") {
|
|
17697
|
+
try {
|
|
17698
|
+
if (!queryEmbedding) queryEmbedding = await embedTextCached(query);
|
|
17699
|
+
embedding = await embedTextCached(r.content);
|
|
17700
|
+
} catch {
|
|
17701
|
+
}
|
|
17702
|
+
}
|
|
17703
|
+
mmrCandidates.push({ id: `${r.type}:${r.id}`, score: r.score, embedding });
|
|
17704
|
+
}
|
|
17705
|
+
const mmrSelected = selectByMmr(mmrCandidates, max_results, diversity);
|
|
17706
|
+
const selectedIds = new Set(mmrSelected.map((m) => m.id));
|
|
17707
|
+
selected = deduped.filter((r) => selectedIds.has(`${r.type}:${r.id}`));
|
|
17708
|
+
const orderMap = new Map(mmrSelected.map((m, i) => [m.id, i]));
|
|
17709
|
+
selected.sort((a, b) => (orderMap.get(`${a.type}:${a.id}`) ?? 0) - (orderMap.get(`${b.type}:${b.id}`) ?? 0));
|
|
17710
|
+
} else {
|
|
17711
|
+
selected = deduped.slice(0, max_results);
|
|
17712
|
+
}
|
|
17713
|
+
if (vaultPath2) {
|
|
17714
|
+
const queryTokens = tokenize(query).map((t) => t.toLowerCase());
|
|
17715
|
+
let queryEmb = null;
|
|
17716
|
+
if (hasEmbeddingsIndex()) {
|
|
17717
|
+
try {
|
|
17718
|
+
queryEmb = await embedTextCached(query);
|
|
17719
|
+
} catch {
|
|
17720
|
+
}
|
|
17721
|
+
}
|
|
17722
|
+
for (const r of selected) {
|
|
17723
|
+
if (r.type !== "note") continue;
|
|
17724
|
+
try {
|
|
17725
|
+
const absPath = vaultPath2 + "/" + r.id;
|
|
17726
|
+
const snippets = await extractBestSnippets(absPath, queryEmb, queryTokens);
|
|
17727
|
+
if (snippets.length > 0 && snippets[0].text.length > 0) {
|
|
17728
|
+
r.content = snippets[0].text;
|
|
17729
|
+
}
|
|
17730
|
+
} catch {
|
|
17731
|
+
}
|
|
17732
|
+
}
|
|
17733
|
+
}
|
|
17523
17734
|
if (max_tokens) {
|
|
17524
17735
|
let tokenBudget = max_tokens;
|
|
17525
17736
|
const budgeted = [];
|
|
17526
|
-
for (const r of
|
|
17737
|
+
for (const r of selected) {
|
|
17527
17738
|
const estimatedTokens = Math.ceil(r.content.length / 4);
|
|
17528
17739
|
if (tokenBudget - estimatedTokens < 0 && budgeted.length > 0) break;
|
|
17529
17740
|
tokenBudget -= estimatedTokens;
|
|
@@ -17531,9 +17742,9 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
17531
17742
|
}
|
|
17532
17743
|
return budgeted;
|
|
17533
17744
|
}
|
|
17534
|
-
return
|
|
17745
|
+
return selected;
|
|
17535
17746
|
}
|
|
17536
|
-
function registerRecallTools(server2, getStateDb) {
|
|
17747
|
+
function registerRecallTools(server2, getStateDb, getVaultPath) {
|
|
17537
17748
|
server2.tool(
|
|
17538
17749
|
"recall",
|
|
17539
17750
|
"Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
|
|
@@ -17542,7 +17753,8 @@ function registerRecallTools(server2, getStateDb) {
|
|
|
17542
17753
|
max_results: z24.number().min(1).max(100).optional().describe("Max results (default: 20)"),
|
|
17543
17754
|
focus: z24.enum(["entities", "notes", "memories", "all"]).optional().describe("Limit search to specific type (default: all)"),
|
|
17544
17755
|
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)")
|
|
17756
|
+
max_tokens: z24.number().optional().describe("Token budget for response (truncates lower-ranked results)"),
|
|
17757
|
+
diversity: z24.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
|
|
17546
17758
|
},
|
|
17547
17759
|
async (args) => {
|
|
17548
17760
|
const stateDb2 = getStateDb();
|
|
@@ -17556,7 +17768,9 @@ function registerRecallTools(server2, getStateDb) {
|
|
|
17556
17768
|
max_results: args.max_results,
|
|
17557
17769
|
focus: args.focus,
|
|
17558
17770
|
entity: args.entity,
|
|
17559
|
-
max_tokens: args.max_tokens
|
|
17771
|
+
max_tokens: args.max_tokens,
|
|
17772
|
+
diversity: args.diversity,
|
|
17773
|
+
vaultPath: getVaultPath?.()
|
|
17560
17774
|
});
|
|
17561
17775
|
const entities = results.filter((r) => r.type === "entity");
|
|
17562
17776
|
const notes = results.filter((r) => r.type === "note");
|
|
@@ -17992,7 +18206,7 @@ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
|
17992
18206
|
|
|
17993
18207
|
// src/tools/write/enrich.ts
|
|
17994
18208
|
import { z as z27 } from "zod";
|
|
17995
|
-
import * as
|
|
18209
|
+
import * as fs30 from "fs/promises";
|
|
17996
18210
|
import * as path30 from "path";
|
|
17997
18211
|
function hasSkipWikilinks(content) {
|
|
17998
18212
|
if (!content.startsWith("---")) return false;
|
|
@@ -18004,7 +18218,7 @@ function hasSkipWikilinks(content) {
|
|
|
18004
18218
|
async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
|
|
18005
18219
|
const results = [];
|
|
18006
18220
|
try {
|
|
18007
|
-
const entries = await
|
|
18221
|
+
const entries = await fs30.readdir(dirPath, { withFileTypes: true });
|
|
18008
18222
|
for (const entry of entries) {
|
|
18009
18223
|
if (entry.name.startsWith(".")) continue;
|
|
18010
18224
|
const fullPath = path30.join(dirPath, entry.name);
|
|
@@ -18079,7 +18293,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
18079
18293
|
const fullPath = path30.join(vaultPath2, relativePath);
|
|
18080
18294
|
let content;
|
|
18081
18295
|
try {
|
|
18082
|
-
content = await
|
|
18296
|
+
content = await fs30.readFile(fullPath, "utf-8");
|
|
18083
18297
|
} catch {
|
|
18084
18298
|
continue;
|
|
18085
18299
|
}
|
|
@@ -18107,7 +18321,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
18107
18321
|
});
|
|
18108
18322
|
if (!dry_run) {
|
|
18109
18323
|
const fullPath = path30.join(vaultPath2, relativePath);
|
|
18110
|
-
await
|
|
18324
|
+
await fs30.writeFile(fullPath, result.content, "utf-8");
|
|
18111
18325
|
notesModified++;
|
|
18112
18326
|
if (stateDb2) {
|
|
18113
18327
|
trackWikilinkApplications(stateDb2, relativePath, entities);
|
|
@@ -18466,7 +18680,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
18466
18680
|
import { z as z30 } from "zod";
|
|
18467
18681
|
|
|
18468
18682
|
// src/core/read/similarity.ts
|
|
18469
|
-
import * as
|
|
18683
|
+
import * as fs31 from "fs";
|
|
18470
18684
|
import * as path31 from "path";
|
|
18471
18685
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
18472
18686
|
"the",
|
|
@@ -18607,7 +18821,7 @@ function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
|
18607
18821
|
const absPath = path31.join(vaultPath2, sourcePath);
|
|
18608
18822
|
let content;
|
|
18609
18823
|
try {
|
|
18610
|
-
content =
|
|
18824
|
+
content = fs31.readFileSync(absPath, "utf-8");
|
|
18611
18825
|
} catch {
|
|
18612
18826
|
return [];
|
|
18613
18827
|
}
|
|
@@ -18685,6 +18899,7 @@ async function findSemanticSimilarNotes(vaultPath2, index, sourcePath, options =
|
|
|
18685
18899
|
}
|
|
18686
18900
|
async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
18687
18901
|
const limit = options.limit ?? 10;
|
|
18902
|
+
const diversity = options.diversity ?? 0.7;
|
|
18688
18903
|
const bm25Results = findSimilarNotes(db4, vaultPath2, index, sourcePath, {
|
|
18689
18904
|
limit: limit * 2,
|
|
18690
18905
|
excludeLinked: options.excludeLinked
|
|
@@ -18716,7 +18931,19 @@ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, option
|
|
|
18716
18931
|
};
|
|
18717
18932
|
});
|
|
18718
18933
|
merged.sort((a, b) => b.score - a.score);
|
|
18719
|
-
|
|
18934
|
+
if (merged.length > limit) {
|
|
18935
|
+
const noteEmbeddings = loadNoteEmbeddingsForPaths(merged.map((m) => m.path));
|
|
18936
|
+
const mmrCandidates = merged.map((m) => ({
|
|
18937
|
+
id: m.path,
|
|
18938
|
+
score: m.score,
|
|
18939
|
+
embedding: noteEmbeddings.get(m.path) ?? null
|
|
18940
|
+
}));
|
|
18941
|
+
const mmrSelected = selectByMmr(mmrCandidates, limit, diversity);
|
|
18942
|
+
const selectedPaths = new Set(mmrSelected.map((m) => m.id));
|
|
18943
|
+
const mergedMap = new Map(merged.map((m) => [m.path, m]));
|
|
18944
|
+
return mmrSelected.map((m) => mergedMap.get(m.id)).filter(Boolean);
|
|
18945
|
+
}
|
|
18946
|
+
return merged;
|
|
18720
18947
|
}
|
|
18721
18948
|
|
|
18722
18949
|
// src/tools/read/similarity.ts
|
|
@@ -18729,10 +18956,11 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
18729
18956
|
inputSchema: {
|
|
18730
18957
|
path: z30.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
|
|
18731
18958
|
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)")
|
|
18959
|
+
exclude_linked: z30.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)"),
|
|
18960
|
+
diversity: z30.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
|
|
18733
18961
|
}
|
|
18734
18962
|
},
|
|
18735
|
-
async ({ path: path33, limit, exclude_linked }) => {
|
|
18963
|
+
async ({ path: path33, limit, exclude_linked, diversity }) => {
|
|
18736
18964
|
const index = getIndex();
|
|
18737
18965
|
const vaultPath2 = getVaultPath();
|
|
18738
18966
|
const stateDb2 = getStateDb();
|
|
@@ -18751,7 +18979,8 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
18751
18979
|
}
|
|
18752
18980
|
const opts = {
|
|
18753
18981
|
limit: limit ?? 10,
|
|
18754
|
-
excludeLinked: exclude_linked ?? true
|
|
18982
|
+
excludeLinked: exclude_linked ?? true,
|
|
18983
|
+
diversity: diversity ?? 0.7
|
|
18755
18984
|
};
|
|
18756
18985
|
const useHybrid = hasEmbeddingsIndex();
|
|
18757
18986
|
const method = useHybrid ? "hybrid" : "bm25";
|
|
@@ -18997,7 +19226,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
18997
19226
|
}
|
|
18998
19227
|
|
|
18999
19228
|
// src/index.ts
|
|
19000
|
-
import * as
|
|
19229
|
+
import * as fs32 from "node:fs/promises";
|
|
19001
19230
|
import { createHash as createHash3 } from "node:crypto";
|
|
19002
19231
|
|
|
19003
19232
|
// src/resources/vault.ts
|
|
@@ -19106,7 +19335,7 @@ function registerVaultResources(server2, getIndex) {
|
|
|
19106
19335
|
// src/index.ts
|
|
19107
19336
|
var __filename = fileURLToPath(import.meta.url);
|
|
19108
19337
|
var __dirname = dirname4(__filename);
|
|
19109
|
-
var pkg = JSON.parse(
|
|
19338
|
+
var pkg = JSON.parse(readFileSync5(join17(__dirname, "../package.json"), "utf-8"));
|
|
19110
19339
|
var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
19111
19340
|
var resolvedVaultPath;
|
|
19112
19341
|
try {
|
|
@@ -19413,7 +19642,7 @@ registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb
|
|
|
19413
19642
|
registerSemanticTools(server, () => vaultPath, () => stateDb);
|
|
19414
19643
|
registerMergeTools2(server, () => stateDb);
|
|
19415
19644
|
registerMemoryTools(server, () => stateDb);
|
|
19416
|
-
registerRecallTools(server, () => stateDb);
|
|
19645
|
+
registerRecallTools(server, () => stateDb, () => vaultPath);
|
|
19417
19646
|
registerBriefTools(server, () => stateDb);
|
|
19418
19647
|
registerVaultResources(server, () => vaultIndex ?? null);
|
|
19419
19648
|
serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
@@ -19559,7 +19788,7 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
|
19559
19788
|
async function scanDir(dir) {
|
|
19560
19789
|
let entries;
|
|
19561
19790
|
try {
|
|
19562
|
-
entries = await
|
|
19791
|
+
entries = await fs32.readdir(dir, { withFileTypes: true });
|
|
19563
19792
|
} catch {
|
|
19564
19793
|
return;
|
|
19565
19794
|
}
|
|
@@ -19570,7 +19799,7 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
|
19570
19799
|
await scanDir(fullPath);
|
|
19571
19800
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
19572
19801
|
try {
|
|
19573
|
-
const stat4 = await
|
|
19802
|
+
const stat4 = await fs32.stat(fullPath);
|
|
19574
19803
|
if (stat4.mtimeMs > sinceMs) {
|
|
19575
19804
|
events.push({
|
|
19576
19805
|
type: "upsert",
|
|
@@ -19773,7 +20002,7 @@ async function runPostIndexWork(index) {
|
|
|
19773
20002
|
continue;
|
|
19774
20003
|
}
|
|
19775
20004
|
try {
|
|
19776
|
-
const content = await
|
|
20005
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
19777
20006
|
const hash = createHash3("sha256").update(content).digest("hex").slice(0, 16);
|
|
19778
20007
|
if (lastContentHashes.get(event.path) === hash) {
|
|
19779
20008
|
serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
|
|
@@ -20211,7 +20440,7 @@ async function runPostIndexWork(index) {
|
|
|
20211
20440
|
for (const event of filteredEvents) {
|
|
20212
20441
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20213
20442
|
try {
|
|
20214
|
-
const content = await
|
|
20443
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20215
20444
|
const zones = getProtectedZones2(content);
|
|
20216
20445
|
const linked = new Set(
|
|
20217
20446
|
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).map((n) => n.toLowerCase())
|
|
@@ -20252,7 +20481,7 @@ async function runPostIndexWork(index) {
|
|
|
20252
20481
|
for (const event of filteredEvents) {
|
|
20253
20482
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20254
20483
|
try {
|
|
20255
|
-
const content = await
|
|
20484
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20256
20485
|
const removed = processImplicitFeedback(stateDb, event.path, content);
|
|
20257
20486
|
for (const entity of removed) feedbackResults.push({ entity, file: event.path });
|
|
20258
20487
|
} catch {
|
|
@@ -20332,7 +20561,7 @@ async function runPostIndexWork(index) {
|
|
|
20332
20561
|
for (const event of filteredEvents) {
|
|
20333
20562
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20334
20563
|
try {
|
|
20335
|
-
const content = await
|
|
20564
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20336
20565
|
const zones = getProtectedZones2(content);
|
|
20337
20566
|
const linkedSet = new Set(
|
|
20338
20567
|
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).concat(forwardLinkResults.find((r) => r.file === event.path)?.dead ?? []).map((n) => n.toLowerCase())
|
|
@@ -20365,7 +20594,7 @@ async function runPostIndexWork(index) {
|
|
|
20365
20594
|
for (const event of filteredEvents) {
|
|
20366
20595
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20367
20596
|
try {
|
|
20368
|
-
const content = await
|
|
20597
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20369
20598
|
const result = await suggestRelatedLinks(content, {
|
|
20370
20599
|
maxSuggestions: 5,
|
|
20371
20600
|
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.74",
|
|
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.74",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|