@velvetmonkey/flywheel-memory 2.0.31 → 2.0.33

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 +151 -36
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2486,7 +2486,8 @@ import {
2486
2486
  var DEFAULT_CONFIG = {
2487
2487
  exclude_task_tags: [],
2488
2488
  exclude_analysis_tags: [],
2489
- exclude_entities: []
2489
+ exclude_entities: [],
2490
+ exclude_entity_folders: []
2490
2491
  };
2491
2492
  function loadConfig(stateDb2) {
2492
2493
  if (stateDb2) {
@@ -3216,6 +3217,7 @@ import {
3216
3217
  getEntityAliases,
3217
3218
  applyWikilinks,
3218
3219
  resolveAliasWikilinks,
3220
+ detectImplicitEntities,
3219
3221
  getEntityIndexFromDb,
3220
3222
  getStateDbMetadata,
3221
3223
  getEntityByName,
@@ -3402,10 +3404,10 @@ function getAllFeedbackBoosts(stateDb2, folder) {
3402
3404
  for (const row of globalRows) {
3403
3405
  let accuracy;
3404
3406
  let sampleCount;
3405
- const fs30 = folderStats?.get(row.entity);
3406
- if (fs30 && fs30.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
3407
- accuracy = fs30.accuracy;
3408
- sampleCount = fs30.count;
3407
+ const fs31 = folderStats?.get(row.entity);
3408
+ if (fs31 && fs31.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
3409
+ accuracy = fs31.accuracy;
3410
+ sampleCount = fs31.count;
3409
3411
  } else {
3410
3412
  accuracy = row.correct_count / row.total;
3411
3413
  sampleCount = row.total;
@@ -5060,6 +5062,52 @@ function processWikilinks(content, notePath) {
5060
5062
  firstOccurrenceOnly: true,
5061
5063
  caseInsensitive: true
5062
5064
  });
5065
+ const implicitMatches = detectImplicitEntities(result.content, {
5066
+ detectImplicit: true,
5067
+ implicitPatterns: ["proper-nouns", "single-caps", "quoted-terms", "camel-case", "acronyms"],
5068
+ minEntityLength: 3
5069
+ });
5070
+ const alreadyLinked = new Set(
5071
+ [...resolved.linkedEntities, ...result.linkedEntities].map((e) => e.toLowerCase())
5072
+ );
5073
+ for (const entity of sortedEntities) {
5074
+ const name = getEntityName2(entity);
5075
+ alreadyLinked.add(name.toLowerCase());
5076
+ const aliases = getEntityAliases(entity);
5077
+ for (const alias of aliases) {
5078
+ alreadyLinked.add(alias.toLowerCase());
5079
+ }
5080
+ }
5081
+ const currentNoteName = notePath ? notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() : null;
5082
+ let newImplicits = implicitMatches.filter((m) => {
5083
+ const normalized = m.text.toLowerCase();
5084
+ if (alreadyLinked.has(normalized)) return false;
5085
+ if (currentNoteName && normalized === currentNoteName) return false;
5086
+ return true;
5087
+ });
5088
+ const nonOverlapping = [];
5089
+ for (const match of newImplicits) {
5090
+ const overlaps = nonOverlapping.some(
5091
+ (existing) => match.start >= existing.start && match.start < existing.end || match.end > existing.start && match.end <= existing.end || match.start <= existing.start && match.end >= existing.end
5092
+ );
5093
+ if (!overlaps) {
5094
+ nonOverlapping.push(match);
5095
+ }
5096
+ }
5097
+ newImplicits = nonOverlapping;
5098
+ if (newImplicits.length > 0) {
5099
+ let processedContent = result.content;
5100
+ for (let i = newImplicits.length - 1; i >= 0; i--) {
5101
+ const m = newImplicits[i];
5102
+ processedContent = processedContent.slice(0, m.start) + `[[${m.text}]]` + processedContent.slice(m.end);
5103
+ }
5104
+ return {
5105
+ content: processedContent,
5106
+ linksAdded: resolved.linksAdded + result.linksAdded + newImplicits.length,
5107
+ linkedEntities: [...resolved.linkedEntities, ...result.linkedEntities],
5108
+ implicitEntities: newImplicits.map((m) => m.text)
5109
+ };
5110
+ }
5063
5111
  return {
5064
5112
  content: result.content,
5065
5113
  linksAdded: resolved.linksAdded + result.linksAdded,
@@ -5076,9 +5124,11 @@ function maybeApplyWikilinks(content, skipWikilinks, notePath) {
5076
5124
  if (moduleStateDb4 && notePath) {
5077
5125
  trackWikilinkApplications(moduleStateDb4, notePath, result.linkedEntities);
5078
5126
  }
5127
+ const implicitCount = result.implicitEntities?.length ?? 0;
5128
+ const implicitInfo = implicitCount > 0 ? ` + ${implicitCount} implicit: ${result.implicitEntities.join(", ")}` : "";
5079
5129
  return {
5080
5130
  content: result.content,
5081
- wikilinkInfo: `Applied ${result.linksAdded} wikilink(s): ${result.linkedEntities.join(", ")}`
5131
+ wikilinkInfo: `Applied ${result.linksAdded} wikilink(s): ${result.linkedEntities.join(", ")}${implicitInfo}`
5082
5132
  };
5083
5133
  }
5084
5134
  return { content: result.content };
@@ -7375,7 +7425,7 @@ function computeEntityDiff(before, after) {
7375
7425
  const alias_changes = [];
7376
7426
  for (const [key, entity] of afterMap) {
7377
7427
  if (!beforeMap.has(key)) {
7378
- added.push(entity.name);
7428
+ added.push({ name: entity.name, category: entity.category, path: entity.path });
7379
7429
  } else {
7380
7430
  const prev = beforeMap.get(key);
7381
7431
  const prevAliases = JSON.stringify(prev.aliases.sort());
@@ -7387,7 +7437,7 @@ function computeEntityDiff(before, after) {
7387
7437
  }
7388
7438
  for (const [key, entity] of beforeMap) {
7389
7439
  if (!afterMap.has(key)) {
7390
- removed.push(entity.name);
7440
+ removed.push({ name: entity.name, category: entity.category, path: entity.path });
7391
7441
  }
7392
7442
  }
7393
7443
  return { added, removed, alias_changes };
@@ -7481,6 +7531,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7481
7531
  trigger: z3.string(),
7482
7532
  duration_ms: z3.number(),
7483
7533
  files_changed: z3.number().nullable(),
7534
+ changed_paths: z3.array(z3.string()).nullable(),
7484
7535
  steps: z3.array(z3.object({
7485
7536
  name: z3.string(),
7486
7537
  duration_ms: z3.number(),
@@ -7490,6 +7541,21 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7490
7541
  skip_reason: z3.string().optional()
7491
7542
  }))
7492
7543
  }).optional().describe("Most recent watcher pipeline run with per-step timing"),
7544
+ recent_pipelines: z3.array(z3.object({
7545
+ timestamp: z3.number(),
7546
+ trigger: z3.string(),
7547
+ duration_ms: z3.number(),
7548
+ files_changed: z3.number().nullable(),
7549
+ changed_paths: z3.array(z3.string()).nullable(),
7550
+ steps: z3.array(z3.object({
7551
+ name: z3.string(),
7552
+ duration_ms: z3.number(),
7553
+ input: z3.record(z3.unknown()),
7554
+ output: z3.record(z3.unknown()),
7555
+ skipped: z3.boolean().optional(),
7556
+ skip_reason: z3.string().optional()
7557
+ }))
7558
+ })).optional().describe("Up to 5 most recent pipeline runs with steps data"),
7493
7559
  fts5_ready: z3.boolean().describe("Whether the FTS5 keyword search index is ready"),
7494
7560
  fts5_building: z3.boolean().describe("Whether the FTS5 keyword search index is currently building"),
7495
7561
  embeddings_building: z3.boolean().describe("Whether semantic embeddings are currently building"),
@@ -7535,7 +7601,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7535
7601
  recommendations.push(`Index is ${Math.floor(indexAge / 60)} minutes old. Consider running refresh_index.`);
7536
7602
  }
7537
7603
  const noteCount = indexBuilt ? index.notes.size : 0;
7538
- const entityCount2 = indexBuilt ? index.entities.size : 0;
7604
+ const entityCount = indexBuilt ? index.entities.size : 0;
7539
7605
  const tagCount = indexBuilt ? index.tags.size : 0;
7540
7606
  let linkCount = 0;
7541
7607
  if (indexBuilt) {
@@ -7587,6 +7653,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7587
7653
  }
7588
7654
  }
7589
7655
  let lastPipeline;
7656
+ let recentPipelines;
7590
7657
  if (stateDb2) {
7591
7658
  try {
7592
7659
  const evt = getRecentPipelineEvent(stateDb2);
@@ -7596,11 +7663,26 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7596
7663
  trigger: evt.trigger,
7597
7664
  duration_ms: evt.duration_ms,
7598
7665
  files_changed: evt.files_changed,
7666
+ changed_paths: evt.changed_paths,
7599
7667
  steps: evt.steps
7600
7668
  };
7601
7669
  }
7602
7670
  } catch {
7603
7671
  }
7672
+ try {
7673
+ const events = getRecentIndexEvents(stateDb2, 10).filter((e) => e.steps && e.steps.length > 0).slice(0, 5);
7674
+ if (events.length > 0) {
7675
+ recentPipelines = events.map((e) => ({
7676
+ timestamp: e.timestamp,
7677
+ trigger: e.trigger,
7678
+ duration_ms: e.duration_ms,
7679
+ files_changed: e.files_changed,
7680
+ changed_paths: e.changed_paths,
7681
+ steps: e.steps
7682
+ }));
7683
+ }
7684
+ } catch {
7685
+ }
7604
7686
  }
7605
7687
  const ftsState = getFTS5State();
7606
7688
  const output = {
@@ -7615,13 +7697,14 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7615
7697
  index_age_seconds: indexAge,
7616
7698
  index_stale: indexStale,
7617
7699
  note_count: noteCount,
7618
- entity_count: entityCount2,
7700
+ entity_count: entityCount,
7619
7701
  tag_count: tagCount,
7620
7702
  link_count: linkCount,
7621
7703
  periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
7622
7704
  config: configInfo,
7623
7705
  last_rebuild: lastRebuild,
7624
7706
  last_pipeline: lastPipeline,
7707
+ recent_pipelines: recentPipelines,
7625
7708
  fts5_ready: ftsState.ready,
7626
7709
  fts5_building: ftsState.building,
7627
7710
  embeddings_building: isEmbeddingsBuilding(),
@@ -14544,7 +14627,7 @@ function computeMetrics(index, stateDb2) {
14544
14627
  }
14545
14628
  }
14546
14629
  const tagCount = index.tags.size;
14547
- const entityCount2 = index.entities.size;
14630
+ const entityCount = index.entities.size;
14548
14631
  const avgLinksPerNote = noteCount > 0 ? linkCount / noteCount : 0;
14549
14632
  const possibleLinks = noteCount * (noteCount - 1);
14550
14633
  const linkDensity = possibleLinks > 0 ? linkCount / possibleLinks : 0;
@@ -14566,7 +14649,7 @@ function computeMetrics(index, stateDb2) {
14566
14649
  link_count: linkCount,
14567
14650
  orphan_count: orphanCount,
14568
14651
  tag_count: tagCount,
14569
- entity_count: entityCount2,
14652
+ entity_count: entityCount,
14570
14653
  avg_links_per_note: Math.round(avgLinksPerNote * 100) / 100,
14571
14654
  link_density: Math.round(linkDensity * 1e4) / 1e4,
14572
14655
  connected_ratio: Math.round(connectedRatio * 1e3) / 1e3,
@@ -15503,6 +15586,9 @@ function registerMergeTools2(server2, getStateDb) {
15503
15586
  );
15504
15587
  }
15505
15588
 
15589
+ // src/index.ts
15590
+ import * as fs30 from "node:fs/promises";
15591
+
15506
15592
  // src/resources/vault.ts
15507
15593
  function registerVaultResources(server2, getIndex) {
15508
15594
  server2.registerResource(
@@ -15995,31 +16081,14 @@ async function main() {
15995
16081
  }
15996
16082
  }
15997
16083
  }
16084
+ var DEFAULT_ENTITY_EXCLUDE_FOLDERS = ["node_modules", "templates", "attachments", "tmp"];
15998
16085
  async function updateEntitiesInStateDb() {
15999
16086
  if (!stateDb) return;
16000
16087
  try {
16088
+ const config = loadConfig(stateDb);
16089
+ const excludeFolders = config.exclude_entity_folders?.length ? config.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
16001
16090
  const entityIndex2 = await scanVaultEntities3(vaultPath, {
16002
- excludeFolders: [
16003
- "daily-notes",
16004
- "daily",
16005
- "weekly",
16006
- "weekly-notes",
16007
- "monthly",
16008
- "monthly-notes",
16009
- "quarterly",
16010
- "yearly-notes",
16011
- "periodic",
16012
- "journal",
16013
- "inbox",
16014
- "templates",
16015
- "attachments",
16016
- "tmp",
16017
- "clippings",
16018
- "readwise",
16019
- "articles",
16020
- "bookmarks",
16021
- "web-clips"
16022
- ]
16091
+ excludeFolders
16023
16092
  });
16024
16093
  stateDb.replaceAllEntities(entityIndex2);
16025
16094
  serverLog("index", `Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
@@ -16138,9 +16207,22 @@ async function runPostIndexWork(index) {
16138
16207
  const entityDiff = computeEntityDiff(entitiesBefore, entitiesAfter);
16139
16208
  tracker.end({ entity_count: entitiesAfter.length, ...entityDiff });
16140
16209
  serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
16141
- tracker.start("hub_scores", { entity_count: entityCount });
16210
+ const hubBefore = /* @__PURE__ */ new Map();
16211
+ if (stateDb) {
16212
+ const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
16213
+ for (const r of rows) hubBefore.set(r.name, r.hub_score);
16214
+ }
16215
+ tracker.start("hub_scores", { entity_count: entitiesAfter.length });
16142
16216
  const hubUpdated = await exportHubScores(vaultIndex, stateDb);
16143
- tracker.end({ updated: hubUpdated ?? 0 });
16217
+ const hubDiffs = [];
16218
+ if (stateDb) {
16219
+ const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
16220
+ for (const r of rows) {
16221
+ const prev = hubBefore.get(r.name) ?? 0;
16222
+ if (prev !== r.hub_score) hubDiffs.push({ entity: r.name, before: prev, after: r.hub_score });
16223
+ }
16224
+ }
16225
+ tracker.end({ updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) });
16144
16226
  serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
16145
16227
  if (hasEmbeddingsIndex()) {
16146
16228
  tracker.start("note_embeddings", { files: batch.events.length });
@@ -16167,6 +16249,7 @@ async function runPostIndexWork(index) {
16167
16249
  if (hasEntityEmbeddingsIndex() && stateDb) {
16168
16250
  tracker.start("entity_embeddings", { files: batch.events.length });
16169
16251
  let entEmbUpdated = 0;
16252
+ const entEmbNames = [];
16170
16253
  try {
16171
16254
  const allEntities = getAllEntitiesFromDb3(stateDb);
16172
16255
  for (const event of batch.events) {
@@ -16180,11 +16263,12 @@ async function runPostIndexWork(index) {
16180
16263
  aliases: entity.aliases
16181
16264
  }, vaultPath);
16182
16265
  entEmbUpdated++;
16266
+ entEmbNames.push(entity.name);
16183
16267
  }
16184
16268
  }
16185
16269
  } catch {
16186
16270
  }
16187
- tracker.end({ updated: entEmbUpdated });
16271
+ tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10) });
16188
16272
  serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated`);
16189
16273
  } else {
16190
16274
  tracker.skip("entity_embeddings", !stateDb ? "no stateDb" : "not built");
@@ -16219,6 +16303,37 @@ async function runPostIndexWork(index) {
16219
16303
  }
16220
16304
  tracker.end({ updated: taskUpdated, removed: taskRemoved });
16221
16305
  serverLog("watcher", `Task cache: ${taskUpdated} updated, ${taskRemoved} removed`);
16306
+ tracker.start("wikilink_check", { files: batch.events.length });
16307
+ const trackedLinks = [];
16308
+ if (stateDb) {
16309
+ for (const event of batch.events) {
16310
+ if (event.type === "delete" || !event.path.endsWith(".md")) continue;
16311
+ try {
16312
+ const apps = getTrackedApplications(stateDb, event.path);
16313
+ if (apps.length > 0) trackedLinks.push({ file: event.path, entities: apps });
16314
+ } catch {
16315
+ }
16316
+ }
16317
+ }
16318
+ tracker.end({ tracked: trackedLinks });
16319
+ serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files`);
16320
+ tracker.start("implicit_feedback", { files: batch.events.length });
16321
+ const feedbackResults = [];
16322
+ if (stateDb) {
16323
+ for (const event of batch.events) {
16324
+ if (event.type === "delete" || !event.path.endsWith(".md")) continue;
16325
+ try {
16326
+ const content = await fs30.readFile(path29.join(vaultPath, event.path), "utf-8");
16327
+ const removed = processImplicitFeedback(stateDb, event.path, content);
16328
+ for (const entity of removed) feedbackResults.push({ entity, file: event.path });
16329
+ } catch {
16330
+ }
16331
+ }
16332
+ }
16333
+ tracker.end({ removals: feedbackResults });
16334
+ if (feedbackResults.length > 0) {
16335
+ serverLog("watcher", `Implicit feedback: ${feedbackResults.length} removals detected`);
16336
+ }
16222
16337
  const duration = Date.now() - batchStart;
16223
16338
  if (stateDb) {
16224
16339
  recordIndexEvent(stateDb, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.31",
3
+ "version": "2.0.33",
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",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@modelcontextprotocol/sdk": "^1.25.1",
53
- "@velvetmonkey/vault-core": "^2.0.31",
53
+ "@velvetmonkey/vault-core": "^2.0.33",
54
54
  "better-sqlite3": "^11.0.0",
55
55
  "chokidar": "^4.0.0",
56
56
  "gray-matter": "^4.0.3",