@velvetmonkey/flywheel-memory 2.0.32 → 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 +107 -31
  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) {
@@ -3403,10 +3404,10 @@ function getAllFeedbackBoosts(stateDb2, folder) {
3403
3404
  for (const row of globalRows) {
3404
3405
  let accuracy;
3405
3406
  let sampleCount;
3406
- const fs30 = folderStats?.get(row.entity);
3407
- if (fs30 && fs30.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
3408
- accuracy = fs30.accuracy;
3409
- 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;
3410
3411
  } else {
3411
3412
  accuracy = row.correct_count / row.total;
3412
3413
  sampleCount = row.total;
@@ -5078,12 +5079,22 @@ function processWikilinks(content, notePath) {
5078
5079
  }
5079
5080
  }
5080
5081
  const currentNoteName = notePath ? notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() : null;
5081
- const newImplicits = implicitMatches.filter((m) => {
5082
+ let newImplicits = implicitMatches.filter((m) => {
5082
5083
  const normalized = m.text.toLowerCase();
5083
5084
  if (alreadyLinked.has(normalized)) return false;
5084
5085
  if (currentNoteName && normalized === currentNoteName) return false;
5085
5086
  return true;
5086
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;
5087
5098
  if (newImplicits.length > 0) {
5088
5099
  let processedContent = result.content;
5089
5100
  for (let i = newImplicits.length - 1; i >= 0; i--) {
@@ -7414,7 +7425,7 @@ function computeEntityDiff(before, after) {
7414
7425
  const alias_changes = [];
7415
7426
  for (const [key, entity] of afterMap) {
7416
7427
  if (!beforeMap.has(key)) {
7417
- added.push(entity.name);
7428
+ added.push({ name: entity.name, category: entity.category, path: entity.path });
7418
7429
  } else {
7419
7430
  const prev = beforeMap.get(key);
7420
7431
  const prevAliases = JSON.stringify(prev.aliases.sort());
@@ -7426,7 +7437,7 @@ function computeEntityDiff(before, after) {
7426
7437
  }
7427
7438
  for (const [key, entity] of beforeMap) {
7428
7439
  if (!afterMap.has(key)) {
7429
- removed.push(entity.name);
7440
+ removed.push({ name: entity.name, category: entity.category, path: entity.path });
7430
7441
  }
7431
7442
  }
7432
7443
  return { added, removed, alias_changes };
@@ -7520,6 +7531,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7520
7531
  trigger: z3.string(),
7521
7532
  duration_ms: z3.number(),
7522
7533
  files_changed: z3.number().nullable(),
7534
+ changed_paths: z3.array(z3.string()).nullable(),
7523
7535
  steps: z3.array(z3.object({
7524
7536
  name: z3.string(),
7525
7537
  duration_ms: z3.number(),
@@ -7529,6 +7541,21 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7529
7541
  skip_reason: z3.string().optional()
7530
7542
  }))
7531
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"),
7532
7559
  fts5_ready: z3.boolean().describe("Whether the FTS5 keyword search index is ready"),
7533
7560
  fts5_building: z3.boolean().describe("Whether the FTS5 keyword search index is currently building"),
7534
7561
  embeddings_building: z3.boolean().describe("Whether semantic embeddings are currently building"),
@@ -7626,6 +7653,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7626
7653
  }
7627
7654
  }
7628
7655
  let lastPipeline;
7656
+ let recentPipelines;
7629
7657
  if (stateDb2) {
7630
7658
  try {
7631
7659
  const evt = getRecentPipelineEvent(stateDb2);
@@ -7635,11 +7663,26 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7635
7663
  trigger: evt.trigger,
7636
7664
  duration_ms: evt.duration_ms,
7637
7665
  files_changed: evt.files_changed,
7666
+ changed_paths: evt.changed_paths,
7638
7667
  steps: evt.steps
7639
7668
  };
7640
7669
  }
7641
7670
  } catch {
7642
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
+ }
7643
7686
  }
7644
7687
  const ftsState = getFTS5State();
7645
7688
  const output = {
@@ -7661,6 +7704,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7661
7704
  config: configInfo,
7662
7705
  last_rebuild: lastRebuild,
7663
7706
  last_pipeline: lastPipeline,
7707
+ recent_pipelines: recentPipelines,
7664
7708
  fts5_ready: ftsState.ready,
7665
7709
  fts5_building: ftsState.building,
7666
7710
  embeddings_building: isEmbeddingsBuilding(),
@@ -15542,6 +15586,9 @@ function registerMergeTools2(server2, getStateDb) {
15542
15586
  );
15543
15587
  }
15544
15588
 
15589
+ // src/index.ts
15590
+ import * as fs30 from "node:fs/promises";
15591
+
15545
15592
  // src/resources/vault.ts
15546
15593
  function registerVaultResources(server2, getIndex) {
15547
15594
  server2.registerResource(
@@ -16034,31 +16081,14 @@ async function main() {
16034
16081
  }
16035
16082
  }
16036
16083
  }
16084
+ var DEFAULT_ENTITY_EXCLUDE_FOLDERS = ["node_modules", "templates", "attachments", "tmp"];
16037
16085
  async function updateEntitiesInStateDb() {
16038
16086
  if (!stateDb) return;
16039
16087
  try {
16088
+ const config = loadConfig(stateDb);
16089
+ const excludeFolders = config.exclude_entity_folders?.length ? config.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
16040
16090
  const entityIndex2 = await scanVaultEntities3(vaultPath, {
16041
- excludeFolders: [
16042
- "daily-notes",
16043
- "daily",
16044
- "weekly",
16045
- "weekly-notes",
16046
- "monthly",
16047
- "monthly-notes",
16048
- "quarterly",
16049
- "yearly-notes",
16050
- "periodic",
16051
- "journal",
16052
- "inbox",
16053
- "templates",
16054
- "attachments",
16055
- "tmp",
16056
- "clippings",
16057
- "readwise",
16058
- "articles",
16059
- "bookmarks",
16060
- "web-clips"
16061
- ]
16091
+ excludeFolders
16062
16092
  });
16063
16093
  stateDb.replaceAllEntities(entityIndex2);
16064
16094
  serverLog("index", `Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
@@ -16177,9 +16207,22 @@ async function runPostIndexWork(index) {
16177
16207
  const entityDiff = computeEntityDiff(entitiesBefore, entitiesAfter);
16178
16208
  tracker.end({ entity_count: entitiesAfter.length, ...entityDiff });
16179
16209
  serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
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
+ }
16180
16215
  tracker.start("hub_scores", { entity_count: entitiesAfter.length });
16181
16216
  const hubUpdated = await exportHubScores(vaultIndex, stateDb);
16182
- 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) });
16183
16226
  serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
16184
16227
  if (hasEmbeddingsIndex()) {
16185
16228
  tracker.start("note_embeddings", { files: batch.events.length });
@@ -16206,6 +16249,7 @@ async function runPostIndexWork(index) {
16206
16249
  if (hasEntityEmbeddingsIndex() && stateDb) {
16207
16250
  tracker.start("entity_embeddings", { files: batch.events.length });
16208
16251
  let entEmbUpdated = 0;
16252
+ const entEmbNames = [];
16209
16253
  try {
16210
16254
  const allEntities = getAllEntitiesFromDb3(stateDb);
16211
16255
  for (const event of batch.events) {
@@ -16219,11 +16263,12 @@ async function runPostIndexWork(index) {
16219
16263
  aliases: entity.aliases
16220
16264
  }, vaultPath);
16221
16265
  entEmbUpdated++;
16266
+ entEmbNames.push(entity.name);
16222
16267
  }
16223
16268
  }
16224
16269
  } catch {
16225
16270
  }
16226
- tracker.end({ updated: entEmbUpdated });
16271
+ tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10) });
16227
16272
  serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated`);
16228
16273
  } else {
16229
16274
  tracker.skip("entity_embeddings", !stateDb ? "no stateDb" : "not built");
@@ -16258,6 +16303,37 @@ async function runPostIndexWork(index) {
16258
16303
  }
16259
16304
  tracker.end({ updated: taskUpdated, removed: taskRemoved });
16260
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
+ }
16261
16337
  const duration = Date.now() - batchStart;
16262
16338
  if (stateDb) {
16263
16339
  recordIndexEvent(stateDb, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.32",
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.32",
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",