@velvetmonkey/flywheel-memory 2.0.37 → 2.0.39

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 +65 -5
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -6986,7 +6986,7 @@ function refreshIfStale(vaultPath2, index, excludeTags) {
6986
6986
  }
6987
6987
 
6988
6988
  // src/index.ts
6989
- import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb3 } from "@velvetmonkey/vault-core";
6989
+ import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb3, findEntityMatches as findEntityMatches2, getProtectedZones as getProtectedZones2, rangeOverlapsProtectedZone } from "@velvetmonkey/vault-core";
6990
6990
 
6991
6991
  // src/tools/read/graph.ts
6992
6992
  import * as fs9 from "fs";
@@ -17369,6 +17369,7 @@ async function runPostIndexWork(index) {
17369
17369
  const entitiesAfter = stateDb ? getAllEntitiesFromDb3(stateDb) : [];
17370
17370
  const entityDiff = computeEntityDiff(entitiesBefore, entitiesAfter);
17371
17371
  const categoryChanges = [];
17372
+ const descriptionChanges = [];
17372
17373
  if (stateDb) {
17373
17374
  const beforeMap = new Map(entitiesBefore.map((e) => [e.name, e]));
17374
17375
  const insertChange = stateDb.db.prepare(
@@ -17380,9 +17381,17 @@ async function runPostIndexWork(index) {
17380
17381
  insertChange.run(after.name, "category", before.category, after.category);
17381
17382
  categoryChanges.push({ entity: after.name, from: before.category, to: after.category });
17382
17383
  }
17384
+ if (before) {
17385
+ const oldDesc = before.description ?? null;
17386
+ const newDesc = after.description ?? null;
17387
+ if (oldDesc !== newDesc) {
17388
+ insertChange.run(after.name, "description", oldDesc, newDesc);
17389
+ descriptionChanges.push({ entity: after.name, from: oldDesc, to: newDesc });
17390
+ }
17391
+ }
17383
17392
  }
17384
17393
  }
17385
- tracker.end({ entity_count: entitiesAfter.length, ...entityDiff, category_changes: categoryChanges });
17394
+ tracker.end({ entity_count: entitiesAfter.length, ...entityDiff, category_changes: categoryChanges, description_changes: descriptionChanges });
17386
17395
  serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
17387
17396
  tracker.start("hub_scores", { entity_count: entitiesAfter.length });
17388
17397
  const hubUpdated = await exportHubScores(vaultIndex, stateDb);
@@ -17521,6 +17530,7 @@ async function runPostIndexWork(index) {
17521
17530
  }
17522
17531
  }
17523
17532
  const linkDiffs = [];
17533
+ const survivedLinks = [];
17524
17534
  if (stateDb) {
17525
17535
  const upsertHistory = stateDb.db.prepare(`
17526
17536
  INSERT INTO note_link_history (note_path, target) VALUES (?, ?)
@@ -17533,6 +17543,9 @@ async function runPostIndexWork(index) {
17533
17543
  const markPositive = stateDb.db.prepare(`
17534
17544
  UPDATE note_link_history SET last_positive_at = datetime('now') WHERE note_path = ? AND target = ?
17535
17545
  `);
17546
+ const getEdgeCount = stateDb.db.prepare(
17547
+ "SELECT edits_survived FROM note_link_history WHERE note_path=? AND target=?"
17548
+ );
17536
17549
  for (const entry of forwardLinkResults) {
17537
17550
  const currentSet = /* @__PURE__ */ new Set([
17538
17551
  ...entry.resolved.map((n) => n.toLowerCase()),
@@ -17551,6 +17564,10 @@ async function runPostIndexWork(index) {
17551
17564
  for (const link of currentSet) {
17552
17565
  if (!previousSet.has(link)) continue;
17553
17566
  upsertHistory.run(entry.file, link);
17567
+ const countRow = getEdgeCount.get(entry.file, link);
17568
+ if (countRow) {
17569
+ survivedLinks.push({ entity: link, file: entry.file, count: countRow.edits_survived });
17570
+ }
17554
17571
  const hit = checkThreshold.get(entry.file, link);
17555
17572
  if (hit) {
17556
17573
  const entity = entitiesAfter.find(
@@ -17572,12 +17589,23 @@ async function runPostIndexWork(index) {
17572
17589
  }
17573
17590
  }
17574
17591
  }
17592
+ const processedFiles = new Set(forwardLinkResults.map((r) => r.file));
17593
+ for (const event of filteredEvents) {
17594
+ if (event.type === "delete" || !event.path.endsWith(".md")) continue;
17595
+ if (processedFiles.has(event.path)) continue;
17596
+ const previousSet = getStoredNoteLinks(stateDb, event.path);
17597
+ if (previousSet.size > 0) {
17598
+ linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
17599
+ updateStoredNoteLinks(stateDb, event.path, /* @__PURE__ */ new Set());
17600
+ }
17601
+ }
17575
17602
  }
17576
17603
  tracker.end({
17577
17604
  total_resolved: totalResolved,
17578
17605
  total_dead: totalDead,
17579
17606
  links: forwardLinkResults,
17580
- link_diffs: linkDiffs
17607
+ link_diffs: linkDiffs,
17608
+ survived: survivedLinks
17581
17609
  });
17582
17610
  serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead`);
17583
17611
  tracker.start("wikilink_check", { files: filteredEvents.length });
@@ -17607,8 +17635,40 @@ async function runPostIndexWork(index) {
17607
17635
  trackedLinks.push({ file: diff.file, entities: diff.added });
17608
17636
  }
17609
17637
  }
17610
- tracker.end({ tracked: trackedLinks });
17611
- serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files`);
17638
+ const mentionResults = [];
17639
+ for (const event of filteredEvents) {
17640
+ if (event.type === "delete" || !event.path.endsWith(".md")) continue;
17641
+ try {
17642
+ const content = await fs30.readFile(path31.join(vaultPath, event.path), "utf-8");
17643
+ const zones = getProtectedZones2(content);
17644
+ const linked = new Set(
17645
+ (forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).map((n) => n.toLowerCase())
17646
+ );
17647
+ const mentions = [];
17648
+ for (const entity of entitiesAfter) {
17649
+ if (linked.has(entity.nameLower)) continue;
17650
+ const matches = findEntityMatches2(content, entity.name, true);
17651
+ const valid = matches.some((m) => !rangeOverlapsProtectedZone(m.start, m.end, zones));
17652
+ if (valid) {
17653
+ mentions.push(entity.name);
17654
+ continue;
17655
+ }
17656
+ for (const alias of entity.aliases ?? []) {
17657
+ const aliasMatches = findEntityMatches2(content, alias, true);
17658
+ if (aliasMatches.some((m) => !rangeOverlapsProtectedZone(m.start, m.end, zones))) {
17659
+ mentions.push(entity.name);
17660
+ break;
17661
+ }
17662
+ }
17663
+ }
17664
+ if (mentions.length > 0) {
17665
+ mentionResults.push({ file: event.path, entities: mentions });
17666
+ }
17667
+ } catch {
17668
+ }
17669
+ }
17670
+ tracker.end({ tracked: trackedLinks, mentions: mentionResults });
17671
+ serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files, ${mentionResults.reduce((s, m) => s + m.entities.length, 0)} unwikified mentions`);
17612
17672
  tracker.start("implicit_feedback", { files: filteredEvents.length });
17613
17673
  const feedbackResults = [];
17614
17674
  if (stateDb) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.37",
3
+ "version": "2.0.39",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. 42 tools for search, backlinks, graph queries, mutations, 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.37",
55
+ "@velvetmonkey/vault-core": "^2.0.39",
56
56
  "better-sqlite3": "^11.0.0",
57
57
  "chokidar": "^4.0.0",
58
58
  "gray-matter": "^4.0.3",
@@ -74,7 +74,7 @@
74
74
  "engines": {
75
75
  "node": ">=18.0.0"
76
76
  },
77
- "license": "AGPL-3.0-only",
77
+ "license": "Apache-2.0",
78
78
  "files": [
79
79
  "dist",
80
80
  "README.md",