@velvetmonkey/flywheel-memory 2.0.72 → 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 +297 -47
- 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";
|
|
@@ -1969,8 +1969,11 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
|
|
|
1969
1969
|
const embedding = await embedText(content);
|
|
1970
1970
|
const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
1971
1971
|
upsert.run(file.path, buf, hash, activeModelConfig.id, Date.now());
|
|
1972
|
-
} catch {
|
|
1972
|
+
} catch (err) {
|
|
1973
1973
|
progress.skipped++;
|
|
1974
|
+
if (progress.skipped <= 3) {
|
|
1975
|
+
console.error(`[Semantic] Failed to embed ${file.path}: ${err instanceof Error ? err.message : err}`);
|
|
1976
|
+
}
|
|
1974
1977
|
}
|
|
1975
1978
|
if (onProgress) onProgress(progress);
|
|
1976
1979
|
}
|
|
@@ -2248,6 +2251,29 @@ function loadEntityEmbeddingsToMemory() {
|
|
|
2248
2251
|
} catch {
|
|
2249
2252
|
}
|
|
2250
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
|
+
}
|
|
2251
2277
|
function getEntityEmbeddingsCount() {
|
|
2252
2278
|
if (!db) return 0;
|
|
2253
2279
|
try {
|
|
@@ -3239,8 +3265,8 @@ async function upsertNote(index, vaultPath2, notePath) {
|
|
|
3239
3265
|
removeNoteFromIndex(index, notePath);
|
|
3240
3266
|
}
|
|
3241
3267
|
const fullPath = path7.join(vaultPath2, notePath);
|
|
3242
|
-
const
|
|
3243
|
-
const stats = await
|
|
3268
|
+
const fs33 = await import("fs/promises");
|
|
3269
|
+
const stats = await fs33.stat(fullPath);
|
|
3244
3270
|
const vaultFile = {
|
|
3245
3271
|
path: notePath,
|
|
3246
3272
|
absolutePath: fullPath,
|
|
@@ -3826,11 +3852,11 @@ function getAllFeedbackBoosts(stateDb2, folder, now) {
|
|
|
3826
3852
|
if (folder !== void 0) {
|
|
3827
3853
|
folderStatsMap = /* @__PURE__ */ new Map();
|
|
3828
3854
|
for (const gs of globalStats) {
|
|
3829
|
-
const
|
|
3830
|
-
if (
|
|
3855
|
+
const fs33 = getWeightedFolderStats(stateDb2, gs.entity, folder, now);
|
|
3856
|
+
if (fs33.rawTotal >= FEEDBACK_BOOST_MIN_SAMPLES) {
|
|
3831
3857
|
folderStatsMap.set(gs.entity, {
|
|
3832
|
-
weightedAccuracy:
|
|
3833
|
-
rawCount:
|
|
3858
|
+
weightedAccuracy: fs33.weightedAccuracy,
|
|
3859
|
+
rawCount: fs33.rawTotal
|
|
3834
3860
|
});
|
|
3835
3861
|
}
|
|
3836
3862
|
}
|
|
@@ -3840,10 +3866,10 @@ function getAllFeedbackBoosts(stateDb2, folder, now) {
|
|
|
3840
3866
|
if (stat4.rawTotal < FEEDBACK_BOOST_MIN_SAMPLES) continue;
|
|
3841
3867
|
let accuracy;
|
|
3842
3868
|
let sampleCount;
|
|
3843
|
-
const
|
|
3844
|
-
if (
|
|
3845
|
-
accuracy =
|
|
3846
|
-
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;
|
|
3847
3873
|
} else {
|
|
3848
3874
|
accuracy = stat4.weightedAccuracy;
|
|
3849
3875
|
sampleCount = stat4.rawTotal;
|
|
@@ -8292,6 +8318,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8292
8318
|
},
|
|
8293
8319
|
async ({ text, limit: requestedLimit, offset, detail }) => {
|
|
8294
8320
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
8321
|
+
requireIndex();
|
|
8295
8322
|
const index = getIndex();
|
|
8296
8323
|
const allMatches = findEntityMatches(text, index.entities);
|
|
8297
8324
|
const matches = allMatches.slice(offset, offset + limit);
|
|
@@ -8389,11 +8416,19 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8389
8416
|
});
|
|
8390
8417
|
const ValidateLinksOutputSchema = {
|
|
8391
8418
|
scope: z2.string().describe('What was validated (note path or "all")'),
|
|
8392
|
-
total_links: z2.coerce.number().describe("Total number of links checked"),
|
|
8393
|
-
valid_links: z2.coerce.number().describe("Number of valid links"),
|
|
8394
|
-
broken_links: z2.coerce.number().describe("Total number of broken links"),
|
|
8419
|
+
total_links: z2.coerce.number().optional().describe("Total number of links checked"),
|
|
8420
|
+
valid_links: z2.coerce.number().optional().describe("Number of valid links"),
|
|
8421
|
+
broken_links: z2.coerce.number().optional().describe("Total number of broken links"),
|
|
8395
8422
|
returned_count: z2.coerce.number().describe("Number of broken links returned (may be limited)"),
|
|
8396
|
-
broken: z2.array(BrokenLinkSchema).describe("List of broken links")
|
|
8423
|
+
broken: z2.array(BrokenLinkSchema).optional().describe("List of broken links"),
|
|
8424
|
+
total_dead_targets: z2.coerce.number().optional().describe("Number of unique dead link targets (group_by_target mode)"),
|
|
8425
|
+
total_broken_links: z2.coerce.number().optional().describe("Total broken links across all targets (group_by_target mode)"),
|
|
8426
|
+
targets: z2.array(z2.object({
|
|
8427
|
+
target: z2.string(),
|
|
8428
|
+
mention_count: z2.coerce.number(),
|
|
8429
|
+
sources: z2.array(z2.string()),
|
|
8430
|
+
suggestion: z2.string().optional()
|
|
8431
|
+
})).optional().describe("Dead link targets grouped by frequency (group_by_target mode)")
|
|
8397
8432
|
};
|
|
8398
8433
|
function findSimilarEntity2(target, entities) {
|
|
8399
8434
|
const targetLower = target.toLowerCase();
|
|
@@ -8425,6 +8460,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8425
8460
|
},
|
|
8426
8461
|
async ({ path: notePath, typos_only, group_by_target, limit: requestedLimit, offset }) => {
|
|
8427
8462
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
8463
|
+
requireIndex();
|
|
8428
8464
|
const index = getIndex();
|
|
8429
8465
|
const allBroken = [];
|
|
8430
8466
|
let totalLinks = 0;
|
|
@@ -8477,12 +8513,13 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8477
8513
|
targetMap.set(key, {
|
|
8478
8514
|
count: 1,
|
|
8479
8515
|
sources: /* @__PURE__ */ new Set([broken2.source]),
|
|
8480
|
-
suggestion: broken2.suggestion
|
|
8516
|
+
suggestion: broken2.suggestion,
|
|
8517
|
+
displayTarget: broken2.target
|
|
8481
8518
|
});
|
|
8482
8519
|
}
|
|
8483
8520
|
}
|
|
8484
|
-
const targets = Array.from(targetMap.
|
|
8485
|
-
target,
|
|
8521
|
+
const targets = Array.from(targetMap.values()).map((data) => ({
|
|
8522
|
+
target: data.displayTarget,
|
|
8486
8523
|
mention_count: data.count,
|
|
8487
8524
|
sources: Array.from(data.sources),
|
|
8488
8525
|
...data.suggestion ? { suggestion: data.suggestion } : {}
|
|
@@ -8530,6 +8567,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8530
8567
|
}
|
|
8531
8568
|
},
|
|
8532
8569
|
async ({ min_frequency, limit: requestedLimit }) => {
|
|
8570
|
+
requireIndex();
|
|
8533
8571
|
const index = getIndex();
|
|
8534
8572
|
const limit = Math.min(requestedLimit ?? 20, 100);
|
|
8535
8573
|
const minFreq = min_frequency ?? 2;
|
|
@@ -8578,6 +8616,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8578
8616
|
}
|
|
8579
8617
|
},
|
|
8580
8618
|
async ({ min_cooccurrence, limit: requestedLimit }) => {
|
|
8619
|
+
requireIndex();
|
|
8581
8620
|
const index = getIndex();
|
|
8582
8621
|
const coocIndex = getCooccurrenceIndex();
|
|
8583
8622
|
const limit = Math.min(requestedLimit ?? 20, 100);
|
|
@@ -11770,6 +11809,10 @@ function isPeriodicNote(notePath) {
|
|
|
11770
11809
|
const folder = notePath.split("/")[0]?.toLowerCase() || "";
|
|
11771
11810
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
11772
11811
|
}
|
|
11812
|
+
function isTemplatePath(notePath) {
|
|
11813
|
+
const folder = notePath.split("/")[0]?.toLowerCase() || "";
|
|
11814
|
+
return folder === "templates" || folder === "template";
|
|
11815
|
+
}
|
|
11773
11816
|
function getExcludedPaths(index, config) {
|
|
11774
11817
|
const excluded = /* @__PURE__ */ new Set();
|
|
11775
11818
|
const excludeTags = new Set((config.exclude_analysis_tags ?? []).map((t) => t.toLowerCase()));
|
|
@@ -11893,7 +11936,9 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
11893
11936
|
}, null, 2) }]
|
|
11894
11937
|
};
|
|
11895
11938
|
}
|
|
11896
|
-
const result = getStaleNotes(index, days, min_backlinks).filter(
|
|
11939
|
+
const result = getStaleNotes(index, days, min_backlinks).filter(
|
|
11940
|
+
(n) => !excludedPaths.has(n.path) && !isPeriodicNote(n.path) && !isTemplatePath(n.path)
|
|
11941
|
+
).slice(0, limit);
|
|
11897
11942
|
return {
|
|
11898
11943
|
content: [{ type: "text", text: JSON.stringify({
|
|
11899
11944
|
analysis: "stale",
|
|
@@ -17342,6 +17387,142 @@ function registerMemoryTools(server2, getStateDb) {
|
|
|
17342
17387
|
// src/tools/read/recall.ts
|
|
17343
17388
|
import { z as z24 } from "zod";
|
|
17344
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
|
|
17345
17526
|
function scoreTextRelevance(query, content) {
|
|
17346
17527
|
const queryTokens = tokenize(query).map((t) => t.toLowerCase());
|
|
17347
17528
|
const queryStems = queryTokens.map((t) => stem(t));
|
|
@@ -17373,7 +17554,9 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
17373
17554
|
max_results = 20,
|
|
17374
17555
|
focus = "all",
|
|
17375
17556
|
entity,
|
|
17376
|
-
max_tokens
|
|
17557
|
+
max_tokens,
|
|
17558
|
+
diversity = 0.7,
|
|
17559
|
+
vaultPath: vaultPath2
|
|
17377
17560
|
} = options;
|
|
17378
17561
|
const results = [];
|
|
17379
17562
|
const recencyIndex2 = loadRecencyFromStateDb();
|
|
@@ -17498,11 +17681,60 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
17498
17681
|
seen.add(key);
|
|
17499
17682
|
return true;
|
|
17500
17683
|
});
|
|
17501
|
-
|
|
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
|
+
}
|
|
17502
17734
|
if (max_tokens) {
|
|
17503
17735
|
let tokenBudget = max_tokens;
|
|
17504
17736
|
const budgeted = [];
|
|
17505
|
-
for (const r of
|
|
17737
|
+
for (const r of selected) {
|
|
17506
17738
|
const estimatedTokens = Math.ceil(r.content.length / 4);
|
|
17507
17739
|
if (tokenBudget - estimatedTokens < 0 && budgeted.length > 0) break;
|
|
17508
17740
|
tokenBudget -= estimatedTokens;
|
|
@@ -17510,9 +17742,9 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
17510
17742
|
}
|
|
17511
17743
|
return budgeted;
|
|
17512
17744
|
}
|
|
17513
|
-
return
|
|
17745
|
+
return selected;
|
|
17514
17746
|
}
|
|
17515
|
-
function registerRecallTools(server2, getStateDb) {
|
|
17747
|
+
function registerRecallTools(server2, getStateDb, getVaultPath) {
|
|
17516
17748
|
server2.tool(
|
|
17517
17749
|
"recall",
|
|
17518
17750
|
"Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
|
|
@@ -17521,7 +17753,8 @@ function registerRecallTools(server2, getStateDb) {
|
|
|
17521
17753
|
max_results: z24.number().min(1).max(100).optional().describe("Max results (default: 20)"),
|
|
17522
17754
|
focus: z24.enum(["entities", "notes", "memories", "all"]).optional().describe("Limit search to specific type (default: all)"),
|
|
17523
17755
|
entity: z24.string().optional().describe("Filter memories by entity association"),
|
|
17524
|
-
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)")
|
|
17525
17758
|
},
|
|
17526
17759
|
async (args) => {
|
|
17527
17760
|
const stateDb2 = getStateDb();
|
|
@@ -17535,7 +17768,9 @@ function registerRecallTools(server2, getStateDb) {
|
|
|
17535
17768
|
max_results: args.max_results,
|
|
17536
17769
|
focus: args.focus,
|
|
17537
17770
|
entity: args.entity,
|
|
17538
|
-
max_tokens: args.max_tokens
|
|
17771
|
+
max_tokens: args.max_tokens,
|
|
17772
|
+
diversity: args.diversity,
|
|
17773
|
+
vaultPath: getVaultPath?.()
|
|
17539
17774
|
});
|
|
17540
17775
|
const entities = results.filter((r) => r.type === "entity");
|
|
17541
17776
|
const notes = results.filter((r) => r.type === "note");
|
|
@@ -17971,7 +18206,7 @@ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
|
17971
18206
|
|
|
17972
18207
|
// src/tools/write/enrich.ts
|
|
17973
18208
|
import { z as z27 } from "zod";
|
|
17974
|
-
import * as
|
|
18209
|
+
import * as fs30 from "fs/promises";
|
|
17975
18210
|
import * as path30 from "path";
|
|
17976
18211
|
function hasSkipWikilinks(content) {
|
|
17977
18212
|
if (!content.startsWith("---")) return false;
|
|
@@ -17983,7 +18218,7 @@ function hasSkipWikilinks(content) {
|
|
|
17983
18218
|
async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
|
|
17984
18219
|
const results = [];
|
|
17985
18220
|
try {
|
|
17986
|
-
const entries = await
|
|
18221
|
+
const entries = await fs30.readdir(dirPath, { withFileTypes: true });
|
|
17987
18222
|
for (const entry of entries) {
|
|
17988
18223
|
if (entry.name.startsWith(".")) continue;
|
|
17989
18224
|
const fullPath = path30.join(dirPath, entry.name);
|
|
@@ -18058,7 +18293,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
18058
18293
|
const fullPath = path30.join(vaultPath2, relativePath);
|
|
18059
18294
|
let content;
|
|
18060
18295
|
try {
|
|
18061
|
-
content = await
|
|
18296
|
+
content = await fs30.readFile(fullPath, "utf-8");
|
|
18062
18297
|
} catch {
|
|
18063
18298
|
continue;
|
|
18064
18299
|
}
|
|
@@ -18086,7 +18321,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
18086
18321
|
});
|
|
18087
18322
|
if (!dry_run) {
|
|
18088
18323
|
const fullPath = path30.join(vaultPath2, relativePath);
|
|
18089
|
-
await
|
|
18324
|
+
await fs30.writeFile(fullPath, result.content, "utf-8");
|
|
18090
18325
|
notesModified++;
|
|
18091
18326
|
if (stateDb2) {
|
|
18092
18327
|
trackWikilinkApplications(stateDb2, relativePath, entities);
|
|
@@ -18445,7 +18680,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
18445
18680
|
import { z as z30 } from "zod";
|
|
18446
18681
|
|
|
18447
18682
|
// src/core/read/similarity.ts
|
|
18448
|
-
import * as
|
|
18683
|
+
import * as fs31 from "fs";
|
|
18449
18684
|
import * as path31 from "path";
|
|
18450
18685
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
18451
18686
|
"the",
|
|
@@ -18586,7 +18821,7 @@ function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
|
18586
18821
|
const absPath = path31.join(vaultPath2, sourcePath);
|
|
18587
18822
|
let content;
|
|
18588
18823
|
try {
|
|
18589
|
-
content =
|
|
18824
|
+
content = fs31.readFileSync(absPath, "utf-8");
|
|
18590
18825
|
} catch {
|
|
18591
18826
|
return [];
|
|
18592
18827
|
}
|
|
@@ -18664,6 +18899,7 @@ async function findSemanticSimilarNotes(vaultPath2, index, sourcePath, options =
|
|
|
18664
18899
|
}
|
|
18665
18900
|
async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
18666
18901
|
const limit = options.limit ?? 10;
|
|
18902
|
+
const diversity = options.diversity ?? 0.7;
|
|
18667
18903
|
const bm25Results = findSimilarNotes(db4, vaultPath2, index, sourcePath, {
|
|
18668
18904
|
limit: limit * 2,
|
|
18669
18905
|
excludeLinked: options.excludeLinked
|
|
@@ -18695,7 +18931,19 @@ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, option
|
|
|
18695
18931
|
};
|
|
18696
18932
|
});
|
|
18697
18933
|
merged.sort((a, b) => b.score - a.score);
|
|
18698
|
-
|
|
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;
|
|
18699
18947
|
}
|
|
18700
18948
|
|
|
18701
18949
|
// src/tools/read/similarity.ts
|
|
@@ -18708,10 +18956,11 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
18708
18956
|
inputSchema: {
|
|
18709
18957
|
path: z30.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
|
|
18710
18958
|
limit: z30.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
|
|
18711
|
-
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)")
|
|
18712
18961
|
}
|
|
18713
18962
|
},
|
|
18714
|
-
async ({ path: path33, limit, exclude_linked }) => {
|
|
18963
|
+
async ({ path: path33, limit, exclude_linked, diversity }) => {
|
|
18715
18964
|
const index = getIndex();
|
|
18716
18965
|
const vaultPath2 = getVaultPath();
|
|
18717
18966
|
const stateDb2 = getStateDb();
|
|
@@ -18730,7 +18979,8 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
18730
18979
|
}
|
|
18731
18980
|
const opts = {
|
|
18732
18981
|
limit: limit ?? 10,
|
|
18733
|
-
excludeLinked: exclude_linked ?? true
|
|
18982
|
+
excludeLinked: exclude_linked ?? true,
|
|
18983
|
+
diversity: diversity ?? 0.7
|
|
18734
18984
|
};
|
|
18735
18985
|
const useHybrid = hasEmbeddingsIndex();
|
|
18736
18986
|
const method = useHybrid ? "hybrid" : "bm25";
|
|
@@ -18976,7 +19226,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
18976
19226
|
}
|
|
18977
19227
|
|
|
18978
19228
|
// src/index.ts
|
|
18979
|
-
import * as
|
|
19229
|
+
import * as fs32 from "node:fs/promises";
|
|
18980
19230
|
import { createHash as createHash3 } from "node:crypto";
|
|
18981
19231
|
|
|
18982
19232
|
// src/resources/vault.ts
|
|
@@ -19085,7 +19335,7 @@ function registerVaultResources(server2, getIndex) {
|
|
|
19085
19335
|
// src/index.ts
|
|
19086
19336
|
var __filename = fileURLToPath(import.meta.url);
|
|
19087
19337
|
var __dirname = dirname4(__filename);
|
|
19088
|
-
var pkg = JSON.parse(
|
|
19338
|
+
var pkg = JSON.parse(readFileSync5(join17(__dirname, "../package.json"), "utf-8"));
|
|
19089
19339
|
var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
19090
19340
|
var resolvedVaultPath;
|
|
19091
19341
|
try {
|
|
@@ -19392,7 +19642,7 @@ registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb
|
|
|
19392
19642
|
registerSemanticTools(server, () => vaultPath, () => stateDb);
|
|
19393
19643
|
registerMergeTools2(server, () => stateDb);
|
|
19394
19644
|
registerMemoryTools(server, () => stateDb);
|
|
19395
|
-
registerRecallTools(server, () => stateDb);
|
|
19645
|
+
registerRecallTools(server, () => stateDb, () => vaultPath);
|
|
19396
19646
|
registerBriefTools(server, () => stateDb);
|
|
19397
19647
|
registerVaultResources(server, () => vaultIndex ?? null);
|
|
19398
19648
|
serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
@@ -19538,7 +19788,7 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
|
19538
19788
|
async function scanDir(dir) {
|
|
19539
19789
|
let entries;
|
|
19540
19790
|
try {
|
|
19541
|
-
entries = await
|
|
19791
|
+
entries = await fs32.readdir(dir, { withFileTypes: true });
|
|
19542
19792
|
} catch {
|
|
19543
19793
|
return;
|
|
19544
19794
|
}
|
|
@@ -19549,7 +19799,7 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
|
19549
19799
|
await scanDir(fullPath);
|
|
19550
19800
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
19551
19801
|
try {
|
|
19552
|
-
const stat4 = await
|
|
19802
|
+
const stat4 = await fs32.stat(fullPath);
|
|
19553
19803
|
if (stat4.mtimeMs > sinceMs) {
|
|
19554
19804
|
events.push({
|
|
19555
19805
|
type: "upsert",
|
|
@@ -19752,7 +20002,7 @@ async function runPostIndexWork(index) {
|
|
|
19752
20002
|
continue;
|
|
19753
20003
|
}
|
|
19754
20004
|
try {
|
|
19755
|
-
const content = await
|
|
20005
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
19756
20006
|
const hash = createHash3("sha256").update(content).digest("hex").slice(0, 16);
|
|
19757
20007
|
if (lastContentHashes.get(event.path) === hash) {
|
|
19758
20008
|
serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
|
|
@@ -20190,7 +20440,7 @@ async function runPostIndexWork(index) {
|
|
|
20190
20440
|
for (const event of filteredEvents) {
|
|
20191
20441
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20192
20442
|
try {
|
|
20193
|
-
const content = await
|
|
20443
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20194
20444
|
const zones = getProtectedZones2(content);
|
|
20195
20445
|
const linked = new Set(
|
|
20196
20446
|
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).map((n) => n.toLowerCase())
|
|
@@ -20231,7 +20481,7 @@ async function runPostIndexWork(index) {
|
|
|
20231
20481
|
for (const event of filteredEvents) {
|
|
20232
20482
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20233
20483
|
try {
|
|
20234
|
-
const content = await
|
|
20484
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20235
20485
|
const removed = processImplicitFeedback(stateDb, event.path, content);
|
|
20236
20486
|
for (const entity of removed) feedbackResults.push({ entity, file: event.path });
|
|
20237
20487
|
} catch {
|
|
@@ -20311,7 +20561,7 @@ async function runPostIndexWork(index) {
|
|
|
20311
20561
|
for (const event of filteredEvents) {
|
|
20312
20562
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20313
20563
|
try {
|
|
20314
|
-
const content = await
|
|
20564
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20315
20565
|
const zones = getProtectedZones2(content);
|
|
20316
20566
|
const linkedSet = new Set(
|
|
20317
20567
|
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).concat(forwardLinkResults.find((r) => r.file === event.path)?.dead ?? []).map((n) => n.toLowerCase())
|
|
@@ -20344,7 +20594,7 @@ async function runPostIndexWork(index) {
|
|
|
20344
20594
|
for (const event of filteredEvents) {
|
|
20345
20595
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
20346
20596
|
try {
|
|
20347
|
-
const content = await
|
|
20597
|
+
const content = await fs32.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
20348
20598
|
const result = await suggestRelatedLinks(content, {
|
|
20349
20599
|
maxSuggestions: 5,
|
|
20350
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",
|