@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.
Files changed (2) hide show
  1. package/dist/index.js +270 -41
  2. 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 readFileSync4, realpathSync } from "fs";
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 fs32 = await import("fs/promises");
3246
- const stats = await fs32.stat(fullPath);
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 fs32 = getWeightedFolderStats(stateDb2, gs.entity, folder, now);
3833
- if (fs32.rawTotal >= FEEDBACK_BOOST_MIN_SAMPLES) {
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: fs32.weightedAccuracy,
3836
- rawCount: fs32.rawTotal
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 fs32 = folderStatsMap?.get(stat4.entity);
3847
- if (fs32 && fs32.rawCount >= FEEDBACK_BOOST_MIN_SAMPLES) {
3848
- accuracy = fs32.weightedAccuracy;
3849
- sampleCount = fs32.rawCount;
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.entries()).map(([target, data]) => ({
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
- const truncated = deduped.slice(0, max_results);
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 truncated) {
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 truncated;
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 fs29 from "fs/promises";
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 fs29.readdir(dirPath, { withFileTypes: true });
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 fs29.readFile(fullPath, "utf-8");
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 fs29.writeFile(fullPath, result.content, "utf-8");
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 fs30 from "fs";
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 = fs30.readFileSync(absPath, "utf-8");
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
- return merged.slice(0, limit);
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 fs31 from "node:fs/promises";
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(readFileSync4(join17(__dirname, "../package.json"), "utf-8"));
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 fs31.readdir(dir, { withFileTypes: true });
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 fs31.stat(fullPath);
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 fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
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 fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
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 fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
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 fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
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 fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
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.73",
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.73",
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",