@velvetmonkey/flywheel-memory 2.0.122 → 2.0.124

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 +218 -83
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6314,7 +6314,11 @@ var indexProgress = { parsed: 0, total: 0 };
6314
6314
  var indexError = null;
6315
6315
  function getIndexState() {
6316
6316
  const scope = getActiveScopeOrNull();
6317
- return scope ? scope.indexState : indexState;
6317
+ if (scope) {
6318
+ if (scope.indexState === "building" && indexState === "ready") return "ready";
6319
+ return scope.indexState;
6320
+ }
6321
+ return indexState;
6318
6322
  }
6319
6323
  function getIndexProgress() {
6320
6324
  return { ...indexProgress };
@@ -10720,7 +10724,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
10720
10724
  import { z as z4 } from "zod";
10721
10725
  init_embeddings();
10722
10726
  import {
10723
- searchEntities,
10727
+ searchEntities as searchEntities2,
10724
10728
  searchEntitiesPrefix
10725
10729
  } from "@velvetmonkey/vault-core";
10726
10730
 
@@ -10729,7 +10733,6 @@ import {
10729
10733
  getEntityByName as getEntityByName2
10730
10734
  } from "@velvetmonkey/vault-core";
10731
10735
  var TOP_LINKS = 10;
10732
- var RECALL_TOP_LINKS = 5;
10733
10736
  function recencyDecay(modifiedDate) {
10734
10737
  if (!modifiedDate) return 0.5;
10735
10738
  const daysSince = (Date.now() - modifiedDate.getTime()) / (1e3 * 60 * 60 * 24);
@@ -10819,6 +10822,63 @@ function rankBacklinks(backlinks, notePath, index, stateDb2, maxLinks = TOP_LINK
10819
10822
  return out;
10820
10823
  }).sort((a, b) => (b.weight ?? 1) - (a.weight ?? 1)).slice(0, maxLinks);
10821
10824
  }
10825
+ var COMPACT_OUTLINK_NAMES = 10;
10826
+ function enrichResultCompact(result, index, stateDb2, opts) {
10827
+ const note = index.notes.get(result.path);
10828
+ const normalizedPath = result.path.toLowerCase().replace(/\.md$/, "");
10829
+ const backlinks = index.backlinks.get(normalizedPath) || [];
10830
+ const enriched = {
10831
+ path: result.path,
10832
+ title: result.title
10833
+ };
10834
+ if (result.snippet) {
10835
+ enriched.snippet = result.snippet;
10836
+ } else {
10837
+ const preview = getContentPreview(result.path);
10838
+ if (preview) enriched.snippet = preview;
10839
+ }
10840
+ if (note) {
10841
+ enriched.backlink_count = backlinks.length;
10842
+ enriched.modified = note.modified.toISOString();
10843
+ if (note.tags.length > 0) enriched.tags = note.tags;
10844
+ if (note.outlinks.length > 0) {
10845
+ enriched.outlink_names = getOutlinkNames(note.outlinks, result.path, index, stateDb2, COMPACT_OUTLINK_NAMES);
10846
+ }
10847
+ }
10848
+ if (stateDb2) {
10849
+ try {
10850
+ const entity = getEntityByName2(stateDb2, result.title);
10851
+ if (entity) {
10852
+ enriched.category = entity.category;
10853
+ enriched.hub_score = entity.hubScore;
10854
+ if (!enriched.snippet && entity.description) {
10855
+ enriched.snippet = entity.description;
10856
+ }
10857
+ }
10858
+ } catch {
10859
+ }
10860
+ }
10861
+ if (opts?.via) enriched.via = opts.via;
10862
+ if (opts?.hop) enriched.hop = opts.hop;
10863
+ return enriched;
10864
+ }
10865
+ function getOutlinkNames(outlinks, notePath, index, stateDb2, max) {
10866
+ const weightMap = /* @__PURE__ */ new Map();
10867
+ if (stateDb2) {
10868
+ try {
10869
+ const rows = stateDb2.db.prepare(
10870
+ "SELECT target, weight, weight_updated_at FROM note_links WHERE note_path = ?"
10871
+ ).all(notePath);
10872
+ for (const row of rows) {
10873
+ const daysSince = row.weight_updated_at ? (Date.now() - row.weight_updated_at) / (1e3 * 60 * 60 * 24) : 0;
10874
+ const decay = Math.max(0.1, 1 - daysSince / 180);
10875
+ weightMap.set(row.target, row.weight * decay);
10876
+ }
10877
+ } catch {
10878
+ }
10879
+ }
10880
+ return outlinks.map((l) => ({ name: l.target, weight: weightMap.get(l.target.toLowerCase()) ?? 1 })).sort((a, b) => b.weight - a.weight).slice(0, max).map((l) => l.name);
10881
+ }
10822
10882
  function enrichResult(result, index, stateDb2) {
10823
10883
  const note = index.notes.get(result.path);
10824
10884
  const normalizedPath = result.path.toLowerCase().replace(/\.md$/, "");
@@ -10886,7 +10946,7 @@ function enrichResultLight(result, index, stateDb2) {
10886
10946
  }
10887
10947
  return enriched;
10888
10948
  }
10889
- function enrichEntityResult(entityName, stateDb2, index) {
10949
+ function enrichEntityCompact(entityName, stateDb2, index) {
10890
10950
  const enriched = {};
10891
10951
  if (stateDb2) {
10892
10952
  try {
@@ -10908,36 +10968,27 @@ function enrichEntityResult(entityName, stateDb2, index) {
10908
10968
  const backlinks = index.backlinks.get(normalizedPath) || [];
10909
10969
  enriched.backlink_count = backlinks.length;
10910
10970
  if (note) {
10911
- enriched.outlink_count = note.outlinks.length;
10912
10971
  if (note.tags.length > 0) enriched.tags = note.tags;
10913
- if (backlinks.length > 0) {
10914
- enriched.top_backlinks = rankBacklinks(backlinks, entityPath, index, stateDb2, RECALL_TOP_LINKS);
10915
- }
10916
10972
  if (note.outlinks.length > 0) {
10917
- enriched.top_outlinks = rankOutlinks(note.outlinks, entityPath, index, stateDb2, RECALL_TOP_LINKS);
10973
+ enriched.outlink_names = getOutlinkNames(note.outlinks, entityPath, index, stateDb2, COMPACT_OUTLINK_NAMES);
10918
10974
  }
10919
10975
  }
10920
10976
  }
10921
10977
  }
10922
10978
  return enriched;
10923
10979
  }
10924
- function enrichNoteResult(notePath, stateDb2, index) {
10980
+ function enrichNoteCompact(notePath, stateDb2, index) {
10925
10981
  const enriched = {};
10926
10982
  if (!index) return enriched;
10927
10983
  const note = index.notes.get(notePath);
10928
10984
  if (!note) return enriched;
10929
10985
  const normalizedPath = notePath.toLowerCase().replace(/\.md$/, "");
10930
10986
  const backlinks = index.backlinks.get(normalizedPath) || [];
10931
- enriched.frontmatter = note.frontmatter;
10932
10987
  if (note.tags.length > 0) enriched.tags = note.tags;
10933
10988
  enriched.backlink_count = backlinks.length;
10934
- enriched.outlink_count = note.outlinks.length;
10935
10989
  enriched.modified = note.modified.toISOString();
10936
- if (backlinks.length > 0) {
10937
- enriched.top_backlinks = rankBacklinks(backlinks, notePath, index, stateDb2, RECALL_TOP_LINKS);
10938
- }
10939
10990
  if (note.outlinks.length > 0) {
10940
- enriched.top_outlinks = rankOutlinks(note.outlinks, notePath, index, stateDb2, RECALL_TOP_LINKS);
10991
+ enriched.outlink_names = getOutlinkNames(note.outlinks, notePath, index, stateDb2, COMPACT_OUTLINK_NAMES);
10941
10992
  }
10942
10993
  if (stateDb2) {
10943
10994
  try {
@@ -10952,8 +11003,132 @@ function enrichNoteResult(notePath, stateDb2, index) {
10952
11003
  return enriched;
10953
11004
  }
10954
11005
 
11006
+ // src/core/read/multihop.ts
11007
+ import { getEntityByName as getEntityByName3, searchEntities } from "@velvetmonkey/vault-core";
11008
+ var DEFAULT_CONFIG2 = {
11009
+ maxParents: 10,
11010
+ maxHops: 2,
11011
+ maxOutlinksPerHop: 10,
11012
+ maxBackfill: 10
11013
+ };
11014
+ function multiHopBackfill(primaryResults, index, stateDb2, config = {}) {
11015
+ const cfg = { ...DEFAULT_CONFIG2, ...config };
11016
+ const seen = new Set(primaryResults.map((r) => r.path).filter(Boolean));
11017
+ const candidates = [];
11018
+ const hop1Results = [];
11019
+ for (const primary of primaryResults.slice(0, cfg.maxParents)) {
11020
+ const primaryPath = primary.path;
11021
+ if (!primaryPath) continue;
11022
+ const note = index.notes.get(primaryPath);
11023
+ if (!note) continue;
11024
+ for (const outlink of note.outlinks.slice(0, cfg.maxOutlinksPerHop)) {
11025
+ const targetPath = index.entities.get(outlink.target.toLowerCase());
11026
+ if (!targetPath || seen.has(targetPath)) continue;
11027
+ seen.add(targetPath);
11028
+ const targetNote = index.notes.get(targetPath);
11029
+ const title = targetNote?.title ?? outlink.target;
11030
+ hop1Results.push({ path: targetPath, title, via: primaryPath });
11031
+ }
11032
+ }
11033
+ for (const h1 of hop1Results) {
11034
+ const enriched = enrichResultCompact(
11035
+ { path: h1.path, title: h1.title },
11036
+ index,
11037
+ stateDb2,
11038
+ { via: h1.via, hop: 1 }
11039
+ );
11040
+ const score = scoreCandidate(h1.path, index, stateDb2);
11041
+ candidates.push({ result: enriched, score });
11042
+ }
11043
+ if (cfg.maxHops >= 2) {
11044
+ for (const h1 of hop1Results) {
11045
+ const note = index.notes.get(h1.path);
11046
+ if (!note) continue;
11047
+ for (const outlink of note.outlinks.slice(0, cfg.maxOutlinksPerHop)) {
11048
+ const targetPath = index.entities.get(outlink.target.toLowerCase());
11049
+ if (!targetPath || seen.has(targetPath)) continue;
11050
+ seen.add(targetPath);
11051
+ const targetNote = index.notes.get(targetPath);
11052
+ const title = targetNote?.title ?? outlink.target;
11053
+ const enriched = enrichResultCompact(
11054
+ { path: targetPath, title },
11055
+ index,
11056
+ stateDb2,
11057
+ { via: h1.path, hop: 2 }
11058
+ );
11059
+ const score = scoreCandidate(targetPath, index, stateDb2);
11060
+ candidates.push({ result: enriched, score });
11061
+ }
11062
+ }
11063
+ }
11064
+ candidates.sort((a, b) => b.score - a.score);
11065
+ return candidates.slice(0, cfg.maxBackfill).map((c) => c.result);
11066
+ }
11067
+ function scoreCandidate(path33, index, stateDb2) {
11068
+ const note = index.notes.get(path33);
11069
+ const decay = recencyDecay(note?.modified);
11070
+ let hubScore = 1;
11071
+ if (stateDb2) {
11072
+ try {
11073
+ const title = note?.title ?? path33.replace(/\.md$/, "").split("/").pop() ?? "";
11074
+ const entity = getEntityByName3(stateDb2, title);
11075
+ if (entity) hubScore = entity.hubScore ?? 1;
11076
+ } catch {
11077
+ }
11078
+ }
11079
+ return hubScore * decay;
11080
+ }
11081
+ function extractExpansionTerms(results, originalQuery, index) {
11082
+ const queryLower = originalQuery.toLowerCase();
11083
+ const terms = /* @__PURE__ */ new Set();
11084
+ for (const r of results.slice(0, 5)) {
11085
+ const outlinks = r.outlink_names;
11086
+ if (outlinks) {
11087
+ for (const name of outlinks) {
11088
+ if (!queryLower.includes(name.toLowerCase()) && index.entities.has(name.toLowerCase())) {
11089
+ terms.add(name);
11090
+ }
11091
+ }
11092
+ }
11093
+ const snippet = r.snippet;
11094
+ if (snippet) {
11095
+ const matches = snippet.match(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g);
11096
+ if (matches) {
11097
+ for (const wl of matches) {
11098
+ const name = wl.replace(/\[\[|\]\]/g, "").split("|")[0];
11099
+ if (!queryLower.includes(name.toLowerCase()) && index.entities.has(name.toLowerCase())) {
11100
+ terms.add(name);
11101
+ }
11102
+ }
11103
+ }
11104
+ }
11105
+ }
11106
+ return Array.from(terms).slice(0, 10);
11107
+ }
11108
+ function expandQuery(expansionTerms, primaryResults, index, stateDb2) {
11109
+ if (!stateDb2 || expansionTerms.length === 0) return [];
11110
+ const seen = new Set(primaryResults.map((r) => r.path).filter(Boolean));
11111
+ const results = [];
11112
+ for (const term of expansionTerms) {
11113
+ try {
11114
+ const entities = searchEntities(stateDb2, term, 3);
11115
+ for (const entity of entities) {
11116
+ if (!entity.path || seen.has(entity.path)) continue;
11117
+ seen.add(entity.path);
11118
+ results.push(enrichResultCompact(
11119
+ { path: entity.path, title: entity.name },
11120
+ index,
11121
+ stateDb2,
11122
+ { via: "query_expansion" }
11123
+ ));
11124
+ }
11125
+ } catch {
11126
+ }
11127
+ }
11128
+ return results;
11129
+ }
11130
+
10955
11131
  // src/tools/read/query.ts
10956
- init_wikilinkFeedback();
10957
11132
  function matchesFrontmatter(note, where) {
10958
11133
  for (const [key, value] of Object.entries(where)) {
10959
11134
  const noteValue = note.frontmatter[key];
@@ -11134,7 +11309,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
11134
11309
  const stateDbEntity = getStateDb2();
11135
11310
  if (stateDbEntity) {
11136
11311
  try {
11137
- entityResults = searchEntities(stateDbEntity, query, limit);
11312
+ entityResults = searchEntities2(stateDbEntity, query, limit);
11138
11313
  } catch {
11139
11314
  }
11140
11315
  }
@@ -11206,46 +11381,22 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
11206
11381
  scored.sort((a, b) => b.rrf_score - a.rrf_score);
11207
11382
  const filtered = applyFolderFilter(scored);
11208
11383
  const stateDb2 = getStateDb2();
11209
- const results = filtered.slice(0, limit).map((item, i) => ({
11210
- ...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
11384
+ const results2 = filtered.slice(0, limit).map((item) => ({
11385
+ ...enrichResultCompact({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
11211
11386
  rrf_score: item.rrf_score,
11212
11387
  in_fts5: item.in_fts5,
11213
11388
  in_semantic: item.in_semantic,
11214
11389
  in_entity: item.in_entity
11215
11390
  }));
11216
- if (stateDb2 && results.length < limit) {
11217
- const existingPaths = new Set(results.map((r) => r.path));
11218
- const backfill = [];
11219
- for (const r of results.slice(0, 3)) {
11220
- const rPath = r.path;
11221
- if (!rPath) continue;
11222
- try {
11223
- const outlinks = getStoredNoteLinks(stateDb2, rPath);
11224
- for (const target of outlinks) {
11225
- const entityRow = stateDb2.db.prepare(
11226
- "SELECT path FROM entities WHERE name_lower = ?"
11227
- ).get(target);
11228
- if (entityRow?.path && !existingPaths.has(entityRow.path)) {
11229
- existingPaths.add(entityRow.path);
11230
- backfill.push({
11231
- ...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2),
11232
- rrf_score: 0,
11233
- in_fts5: false,
11234
- in_semantic: false,
11235
- in_entity: false
11236
- });
11237
- }
11238
- }
11239
- } catch {
11240
- }
11241
- }
11242
- results.push(...backfill.slice(0, limit - results.length));
11243
- }
11391
+ const hopResults2 = multiHopBackfill(results2, index, stateDb2, { maxBackfill: limit });
11392
+ const expansionTerms2 = extractExpansionTerms(results2, query, index);
11393
+ const expansionResults2 = expandQuery(expansionTerms2, [...results2, ...hopResults2], index, stateDb2);
11394
+ results2.push(...hopResults2, ...expansionResults2);
11244
11395
  return { content: [{ type: "text", text: JSON.stringify({
11245
11396
  method: "hybrid",
11246
11397
  query,
11247
11398
  total_results: filtered.length,
11248
- results
11399
+ results: results2
11249
11400
  }, null, 2) }] };
11250
11401
  } catch (err) {
11251
11402
  console.error("[Semantic] Hybrid search failed, falling back to FTS5:", err instanceof Error ? err.message : err);
@@ -11262,49 +11413,33 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
11262
11413
  const filtered = applyFolderFilter(mergedItems);
11263
11414
  const stateDb2 = getStateDb2();
11264
11415
  const sliced = filtered.slice(0, limit);
11265
- const results = sliced.map((item, i) => ({
11266
- ...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
11416
+ const results2 = sliced.map((item) => ({
11417
+ ...enrichResultCompact({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
11267
11418
  ..."in_fts5" in item ? { in_fts5: true } : { in_entity: true }
11268
11419
  }));
11269
- if (stateDb2 && results.length < limit) {
11270
- const existingPaths = new Set(results.map((r) => r.path));
11271
- const backfill = [];
11272
- for (const r of results.slice(0, 3)) {
11273
- const rPath = r.path;
11274
- if (!rPath) continue;
11275
- try {
11276
- const outlinks = getStoredNoteLinks(stateDb2, rPath);
11277
- for (const target of outlinks) {
11278
- const entityRow = stateDb2.db.prepare(
11279
- "SELECT path FROM entities WHERE name_lower = ?"
11280
- ).get(target);
11281
- if (entityRow?.path && !existingPaths.has(entityRow.path)) {
11282
- existingPaths.add(entityRow.path);
11283
- backfill.push({
11284
- ...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2)
11285
- });
11286
- }
11287
- }
11288
- } catch {
11289
- }
11290
- }
11291
- results.push(...backfill.slice(0, limit - results.length));
11292
- }
11420
+ const hopResults2 = multiHopBackfill(results2, index, stateDb2, { maxBackfill: limit });
11421
+ const expansionTerms2 = extractExpansionTerms(results2, query, index);
11422
+ const expansionResults2 = expandQuery(expansionTerms2, [...results2, ...hopResults2], index, stateDb2);
11423
+ results2.push(...hopResults2, ...expansionResults2);
11293
11424
  return { content: [{ type: "text", text: JSON.stringify({
11294
11425
  method: "fts5",
11295
11426
  query,
11296
11427
  total_results: filtered.length,
11297
- results
11428
+ results: results2
11298
11429
  }, null, 2) }] };
11299
11430
  }
11300
11431
  const stateDbFts = getStateDb2();
11301
11432
  const fts5Filtered = applyFolderFilter(fts5Results);
11302
- const enrichedFts5 = fts5Filtered.map((r, i) => ({ ...(i < detailN ? enrichResult : enrichResultLight)({ path: r.path, title: r.title, snippet: r.snippet }, index, stateDbFts), in_fts5: true }));
11433
+ const results = fts5Filtered.map((r) => ({ ...enrichResultCompact({ path: r.path, title: r.title, snippet: r.snippet }, index, stateDbFts), in_fts5: true }));
11434
+ const hopResults = multiHopBackfill(results, index, stateDbFts, { maxBackfill: limit });
11435
+ const expansionTerms = extractExpansionTerms(results, query, index);
11436
+ const expansionResults = expandQuery(expansionTerms, [...results, ...hopResults], index, stateDbFts);
11437
+ results.push(...hopResults, ...expansionResults);
11303
11438
  return { content: [{ type: "text", text: JSON.stringify({
11304
11439
  method: "fts5",
11305
11440
  query,
11306
- total_results: enrichedFts5.length,
11307
- results: enrichedFts5
11441
+ total_results: results.length,
11442
+ results
11308
11443
  }, null, 2) }] };
11309
11444
  }
11310
11445
  return { content: [{ type: "text", text: JSON.stringify({ error: "Provide a query or metadata filters (where, has_tag, folder, etc.)" }, null, 2) }] };
@@ -12030,7 +12165,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
12030
12165
  }
12031
12166
 
12032
12167
  // src/tools/read/primitives.ts
12033
- import { getEntityByName as getEntityByName3 } from "@velvetmonkey/vault-core";
12168
+ import { getEntityByName as getEntityByName4 } from "@velvetmonkey/vault-core";
12034
12169
  function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb2 = () => null) {
12035
12170
  server2.registerTool(
12036
12171
  "get_note_structure",
@@ -12073,7 +12208,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
12073
12208
  const stateDb2 = getStateDb2();
12074
12209
  if (stateDb2 && note) {
12075
12210
  try {
12076
- const entity = getEntityByName3(stateDb2, note.title);
12211
+ const entity = getEntityByName4(stateDb2, note.title);
12077
12212
  if (entity) {
12078
12213
  enriched.category = entity.category;
12079
12214
  enriched.hub_score = entity.hubScore;
@@ -19325,13 +19460,13 @@ function registerRecallTools(server2, getStateDb2, getVaultPath, getIndex) {
19325
19460
  description: e.content,
19326
19461
  score: Math.round(e.score * 10) / 10,
19327
19462
  breakdown: e.breakdown,
19328
- ...enrichEntityResult(e.id, stateDb2, index)
19463
+ ...enrichEntityCompact(e.id, stateDb2, index)
19329
19464
  })),
19330
19465
  notes: notes.map((n) => ({
19331
19466
  path: n.id,
19332
19467
  snippet: n.content,
19333
19468
  score: Math.round(n.score * 10) / 10,
19334
- ...enrichNoteResult(n.id, stateDb2, index)
19469
+ ...enrichNoteCompact(n.id, stateDb2, index)
19335
19470
  })),
19336
19471
  memories: memories.map((m) => ({
19337
19472
  key: m.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.122",
3
+ "version": "2.0.124",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 69 tools for search, backlinks, graph queries, mutations, agent memory, and hybrid semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",