@velvetmonkey/flywheel-memory 2.0.22 → 2.0.24

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 +170 -15
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1839,6 +1839,7 @@ var MAX_FILE_SIZE2 = 5 * 1024 * 1024;
1839
1839
  var db = null;
1840
1840
  var pipeline = null;
1841
1841
  var initPromise = null;
1842
+ var embeddingsBuilding = false;
1842
1843
  var embeddingCache = /* @__PURE__ */ new Map();
1843
1844
  var EMBEDDING_CACHE_MAX = 500;
1844
1845
  var entityEmbeddingsMap = /* @__PURE__ */ new Map();
@@ -1894,6 +1895,7 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
1894
1895
  if (!db) {
1895
1896
  throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
1896
1897
  }
1898
+ embeddingsBuilding = true;
1897
1899
  await initEmbeddings();
1898
1900
  const files = await scanVault(vaultPath2);
1899
1901
  const indexable = files.filter((f) => shouldIndexFile(f.path));
@@ -1937,6 +1939,7 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
1937
1939
  deleteStmt.run(existingPath);
1938
1940
  }
1939
1941
  }
1942
+ embeddingsBuilding = false;
1940
1943
  console.error(`[Semantic] Indexed ${progress.current - progress.skipped} notes, skipped ${progress.skipped}`);
1941
1944
  return progress;
1942
1945
  }
@@ -2034,6 +2037,12 @@ function reciprocalRankFusion(...lists) {
2034
2037
  }
2035
2038
  return scores;
2036
2039
  }
2040
+ function isEmbeddingsBuilding() {
2041
+ return embeddingsBuilding;
2042
+ }
2043
+ function setEmbeddingsBuilding(value) {
2044
+ embeddingsBuilding = value;
2045
+ }
2037
2046
  function hasEmbeddingsIndex() {
2038
2047
  if (!db) return false;
2039
2048
  try {
@@ -5010,6 +5019,15 @@ function getEntityIndexStats() {
5010
5019
  organizations: entityIndex.organizations?.length ?? 0,
5011
5020
  locations: entityIndex.locations?.length ?? 0,
5012
5021
  concepts: entityIndex.concepts?.length ?? 0,
5022
+ animals: entityIndex.animals?.length ?? 0,
5023
+ media: entityIndex.media?.length ?? 0,
5024
+ events: entityIndex.events?.length ?? 0,
5025
+ documents: entityIndex.documents?.length ?? 0,
5026
+ vehicles: entityIndex.vehicles?.length ?? 0,
5027
+ health: entityIndex.health?.length ?? 0,
5028
+ finance: entityIndex.finance?.length ?? 0,
5029
+ food: entityIndex.food?.length ?? 0,
5030
+ hobbies: entityIndex.hobbies?.length ?? 0,
5013
5031
  other: entityIndex.other.length
5014
5032
  }
5015
5033
  };
@@ -5146,14 +5164,32 @@ var DEFAULT_STRICTNESS = "conservative";
5146
5164
  var TYPE_BOOST = {
5147
5165
  people: 5,
5148
5166
  // Names are high value for connections
5167
+ animals: 3,
5168
+ // Pets and animals are personal and specific
5149
5169
  projects: 3,
5150
5170
  // Projects provide context
5151
5171
  organizations: 2,
5152
5172
  // Companies/teams relevant
5173
+ events: 2,
5174
+ // Meetings, trips, milestones
5175
+ media: 2,
5176
+ // Movies, books, shows
5177
+ health: 2,
5178
+ // Medical, fitness — personal relevance
5179
+ vehicles: 2,
5180
+ // Cars, bikes — specific items
5153
5181
  locations: 1,
5154
5182
  // Geographic context
5155
5183
  concepts: 1,
5156
5184
  // Abstract concepts
5185
+ documents: 1,
5186
+ // Reports, guides
5187
+ food: 1,
5188
+ // Recipes, restaurants
5189
+ hobbies: 1,
5190
+ // Crafts, sports
5191
+ finance: 1,
5192
+ // Accounts, budgets
5157
5193
  technologies: 0,
5158
5194
  // Common, avoid over-suggesting
5159
5195
  acronyms: 0,
@@ -5197,14 +5233,24 @@ var CONTEXT_BOOST = {
5197
5233
  daily: {
5198
5234
  people: 5,
5199
5235
  // Daily notes often mention people
5200
- projects: 2
5236
+ animals: 3,
5237
+ // Pets in daily life
5238
+ events: 3,
5239
+ // Daily events and meetings
5240
+ projects: 2,
5201
5241
  // Work updates reference projects
5242
+ food: 2,
5243
+ // Meals and recipes in daily logs
5244
+ health: 2
5245
+ // Fitness and wellness tracking
5202
5246
  },
5203
5247
  project: {
5204
5248
  projects: 5,
5205
5249
  // Project docs reference other projects
5206
- technologies: 2
5250
+ technologies: 2,
5207
5251
  // Technical dependencies
5252
+ documents: 2
5253
+ // Reference documents
5208
5254
  },
5209
5255
  tech: {
5210
5256
  technologies: 5,
@@ -5722,9 +5768,18 @@ var EXCLUDED_DIRS3 = /* @__PURE__ */ new Set([
5722
5768
  ]);
5723
5769
  var MAX_INDEX_FILE_SIZE = 5 * 1024 * 1024;
5724
5770
  var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
5771
+ function splitFrontmatter(raw) {
5772
+ if (!raw.startsWith("---")) return { frontmatter: "", body: raw };
5773
+ const end = raw.indexOf("\n---", 3);
5774
+ if (end === -1) return { frontmatter: "", body: raw };
5775
+ const yaml = raw.substring(4, end);
5776
+ const values = yaml.split("\n").map((line) => line.replace(/^[\s-]*/, "").replace(/^[\w]+:\s*/, "")).filter((v) => v && !v.startsWith("[") && !v.startsWith("{")).join(" ");
5777
+ return { frontmatter: values, body: raw.substring(end + 4) };
5778
+ }
5725
5779
  var db2 = null;
5726
5780
  var state = {
5727
5781
  ready: false,
5782
+ building: false,
5728
5783
  lastBuilt: null,
5729
5784
  noteCount: 0,
5730
5785
  error: null
@@ -5740,6 +5795,7 @@ function setFTS5Database(database) {
5740
5795
  const countRow = db2.prepare("SELECT COUNT(*) as count FROM notes_fts").get();
5741
5796
  state = {
5742
5797
  ready: countRow.count > 0,
5798
+ building: false,
5743
5799
  lastBuilt,
5744
5800
  noteCount: countRow.count,
5745
5801
  error: null
@@ -5755,6 +5811,7 @@ function shouldIndexFile2(filePath) {
5755
5811
  async function buildFTS5Index(vaultPath2) {
5756
5812
  try {
5757
5813
  state.error = null;
5814
+ state.building = true;
5758
5815
  if (!db2) {
5759
5816
  throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
5760
5817
  }
@@ -5762,7 +5819,7 @@ async function buildFTS5Index(vaultPath2) {
5762
5819
  const files = await scanVault(vaultPath2);
5763
5820
  const indexableFiles = files.filter((f) => shouldIndexFile2(f.path));
5764
5821
  const insert = db2.prepare(
5765
- "INSERT INTO notes_fts (path, title, content) VALUES (?, ?, ?)"
5822
+ "INSERT INTO notes_fts (path, title, frontmatter, content) VALUES (?, ?, ?, ?)"
5766
5823
  );
5767
5824
  const insertMany = db2.transaction((filesToIndex) => {
5768
5825
  let indexed2 = 0;
@@ -5772,9 +5829,10 @@ async function buildFTS5Index(vaultPath2) {
5772
5829
  if (stats.size > MAX_INDEX_FILE_SIZE) {
5773
5830
  continue;
5774
5831
  }
5775
- const content = fs7.readFileSync(file.absolutePath, "utf-8");
5832
+ const raw = fs7.readFileSync(file.absolutePath, "utf-8");
5833
+ const { frontmatter, body } = splitFrontmatter(raw);
5776
5834
  const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
5777
- insert.run(file.path, title, content);
5835
+ insert.run(file.path, title, frontmatter, body);
5778
5836
  indexed2++;
5779
5837
  } catch (err) {
5780
5838
  console.error(`[FTS5] Skipping ${file.path}:`, err);
@@ -5789,6 +5847,7 @@ async function buildFTS5Index(vaultPath2) {
5789
5847
  ).run("last_built", now.toISOString());
5790
5848
  state = {
5791
5849
  ready: true,
5850
+ building: false,
5792
5851
  lastBuilt: now,
5793
5852
  noteCount: indexed,
5794
5853
  error: null
@@ -5798,6 +5857,7 @@ async function buildFTS5Index(vaultPath2) {
5798
5857
  } catch (err) {
5799
5858
  state = {
5800
5859
  ready: false,
5860
+ building: false,
5801
5861
  lastBuilt: null,
5802
5862
  noteCount: 0,
5803
5863
  error: err instanceof Error ? err.message : String(err)
@@ -5832,10 +5892,10 @@ function searchFTS5(_vaultPath, query, limit = 10) {
5832
5892
  SELECT
5833
5893
  path,
5834
5894
  title,
5835
- snippet(notes_fts, 2, '[', ']', '...', 20) as snippet
5895
+ snippet(notes_fts, 3, '<mark>', '</mark>', '...', 20) as snippet
5836
5896
  FROM notes_fts
5837
5897
  WHERE notes_fts MATCH ?
5838
- ORDER BY rank
5898
+ ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
5839
5899
  LIMIT ?
5840
5900
  `);
5841
5901
  const results = stmt.all(query, limit);
@@ -6766,6 +6826,9 @@ function getActivitySummary(index, days) {
6766
6826
  };
6767
6827
  }
6768
6828
 
6829
+ // src/tools/read/health.ts
6830
+ import { SCHEMA_VERSION } from "@velvetmonkey/vault-core";
6831
+
6769
6832
  // src/core/shared/indexActivity.ts
6770
6833
  function recordIndexEvent(stateDb2, event) {
6771
6834
  stateDb2.db.prepare(
@@ -6858,6 +6921,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6858
6921
  });
6859
6922
  const HealthCheckOutputSchema = {
6860
6923
  status: z3.enum(["healthy", "degraded", "unhealthy"]).describe("Overall health status"),
6924
+ schema_version: z3.coerce.number().describe("StateDb schema version"),
6861
6925
  vault_accessible: z3.boolean().describe("Whether the vault path is accessible"),
6862
6926
  vault_path: z3.string().describe("The vault path being used"),
6863
6927
  index_state: z3.enum(["building", "ready", "error"]).describe("Current state of the vault index"),
@@ -6877,6 +6941,11 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6877
6941
  duration_ms: z3.number(),
6878
6942
  ago_seconds: z3.number()
6879
6943
  }).optional().describe("Most recent index rebuild event"),
6944
+ fts5_ready: z3.boolean().describe("Whether the FTS5 keyword search index is ready"),
6945
+ fts5_building: z3.boolean().describe("Whether the FTS5 keyword search index is currently building"),
6946
+ embeddings_building: z3.boolean().describe("Whether semantic embeddings are currently building"),
6947
+ embeddings_ready: z3.boolean().describe("Whether semantic embeddings have been built (enables hybrid keyword+semantic search)"),
6948
+ embeddings_count: z3.coerce.number().describe("Number of notes with semantic embeddings"),
6880
6949
  recommendations: z3.array(z3.string()).describe("Suggested actions if any issues detected")
6881
6950
  };
6882
6951
  server2.registerTool(
@@ -6962,8 +7031,10 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6962
7031
  } catch {
6963
7032
  }
6964
7033
  }
7034
+ const ftsState = getFTS5State();
6965
7035
  const output = {
6966
7036
  status,
7037
+ schema_version: SCHEMA_VERSION,
6967
7038
  vault_accessible: vaultAccessible,
6968
7039
  vault_path: vaultPath2,
6969
7040
  index_state: indexState2,
@@ -6978,6 +7049,11 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6978
7049
  periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
6979
7050
  config: configInfo,
6980
7051
  last_rebuild: lastRebuild,
7052
+ fts5_ready: ftsState.ready,
7053
+ fts5_building: ftsState.building,
7054
+ embeddings_building: isEmbeddingsBuilding(),
7055
+ embeddings_ready: hasEmbeddingsIndex(),
7056
+ embeddings_count: getEmbeddingsCount(),
6981
7057
  recommendations
6982
7058
  };
6983
7059
  return {
@@ -7320,27 +7396,55 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
7320
7396
  return { content: [{ type: "text", text: JSON.stringify({ error: "query is required for content search" }, null, 2) }] };
7321
7397
  }
7322
7398
  const ftsState = getFTS5State();
7399
+ if (ftsState.building) {
7400
+ return { content: [{ type: "text", text: JSON.stringify({
7401
+ scope,
7402
+ method: "fts5",
7403
+ query,
7404
+ building: true,
7405
+ total_results: 0,
7406
+ results: [],
7407
+ message: "Search index is building, try again shortly"
7408
+ }, null, 2) }] };
7409
+ }
7323
7410
  if (!ftsState.ready || isIndexStale(vaultPath2)) {
7324
7411
  console.error("[FTS5] Index stale or missing, rebuilding...");
7325
7412
  await buildFTS5Index(vaultPath2);
7326
7413
  }
7327
7414
  const fts5Results = searchFTS5(vaultPath2, query, limit);
7415
+ let entityResults = [];
7416
+ if (scope === "all") {
7417
+ const stateDb2 = getStateDb();
7418
+ if (stateDb2) {
7419
+ try {
7420
+ entityResults = searchEntities(stateDb2, query, limit);
7421
+ } catch {
7422
+ }
7423
+ }
7424
+ }
7328
7425
  if (hasEmbeddingsIndex()) {
7329
7426
  try {
7330
7427
  const semanticResults = await semanticSearch(query, limit);
7331
7428
  const fts5Ranked = fts5Results.map((r) => ({ path: r.path, title: r.title, snippet: r.snippet }));
7332
7429
  const semanticRanked = semanticResults.map((r) => ({ path: r.path, title: r.title }));
7333
- const rrfScores = reciprocalRankFusion(fts5Ranked, semanticRanked);
7334
- const allPaths = /* @__PURE__ */ new Set([...fts5Results.map((r) => r.path), ...semanticResults.map((r) => r.path)]);
7430
+ const entityRanked = entityResults.map((r) => ({ path: r.path, title: r.name }));
7431
+ const rrfScores = reciprocalRankFusion(fts5Ranked, semanticRanked, entityRanked);
7432
+ const allPaths = /* @__PURE__ */ new Set([
7433
+ ...fts5Results.map((r) => r.path),
7434
+ ...semanticResults.map((r) => r.path),
7435
+ ...entityResults.map((r) => r.path)
7436
+ ]);
7335
7437
  const fts5Map = new Map(fts5Results.map((r) => [r.path, r]));
7336
7438
  const semanticMap = new Map(semanticResults.map((r) => [r.path, r]));
7439
+ const entityMap = new Map(entityResults.map((r) => [r.path, r]));
7337
7440
  const merged = Array.from(allPaths).map((p) => ({
7338
7441
  path: p,
7339
- title: fts5Map.get(p)?.title || semanticMap.get(p)?.title || p.replace(/\.md$/, "").split("/").pop() || p,
7442
+ title: fts5Map.get(p)?.title || semanticMap.get(p)?.title || entityMap.get(p)?.name || p.replace(/\.md$/, "").split("/").pop() || p,
7340
7443
  snippet: fts5Map.get(p)?.snippet,
7341
7444
  rrf_score: Math.round((rrfScores.get(p) || 0) * 1e4) / 1e4,
7342
7445
  in_fts5: fts5Map.has(p),
7343
- in_semantic: semanticMap.has(p)
7446
+ in_semantic: semanticMap.has(p),
7447
+ in_entity: entityMap.has(p)
7344
7448
  }));
7345
7449
  merged.sort((a, b) => b.rrf_score - a.rrf_score);
7346
7450
  return { content: [{ type: "text", text: JSON.stringify({
@@ -7354,6 +7458,21 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
7354
7458
  console.error("[Semantic] Hybrid search failed, falling back to FTS5:", err instanceof Error ? err.message : err);
7355
7459
  }
7356
7460
  }
7461
+ if (entityResults.length > 0) {
7462
+ const fts5Map = new Map(fts5Results.map((r) => [r.path, r]));
7463
+ const entityRanked = entityResults.filter((r) => !fts5Map.has(r.path));
7464
+ const merged = [
7465
+ ...fts5Results.map((r) => ({ path: r.path, title: r.title, snippet: r.snippet, in_entity: fts5Map.has(r.path) && entityResults.some((e) => e.path === r.path) })),
7466
+ ...entityRanked.map((r) => ({ path: r.path, title: r.name, snippet: void 0, in_entity: true }))
7467
+ ];
7468
+ return { content: [{ type: "text", text: JSON.stringify({
7469
+ scope: "content",
7470
+ method: "fts5",
7471
+ query,
7472
+ total_results: merged.length,
7473
+ results: merged.slice(0, limit)
7474
+ }, null, 2) }] };
7475
+ }
7357
7476
  return { content: [{ type: "text", text: JSON.stringify({
7358
7477
  scope: "content",
7359
7478
  method: "fts5",
@@ -14147,11 +14266,11 @@ function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14147
14266
  SELECT
14148
14267
  path,
14149
14268
  title,
14150
- bm25(notes_fts) as score,
14151
- snippet(notes_fts, 2, '[', ']', '...', 15) as snippet
14269
+ bm25(notes_fts, 0.0, 5.0, 10.0, 1.0) as score,
14270
+ snippet(notes_fts, 3, '[', ']', '...', 15) as snippet
14152
14271
  FROM notes_fts
14153
14272
  WHERE notes_fts MATCH ?
14154
- ORDER BY rank
14273
+ ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
14155
14274
  LIMIT ?
14156
14275
  `).all(query, limit + 20);
14157
14276
  let filtered = results.filter((r) => r.path !== sourcePath);
@@ -14763,7 +14882,6 @@ async function main() {
14763
14882
  setEmbeddingsDatabase(stateDb.db);
14764
14883
  loadEntityEmbeddingsToMemory();
14765
14884
  setWriteStateDb(stateDb);
14766
- await initializeEntityIndex(vaultPath);
14767
14885
  } catch (err) {
14768
14886
  const msg = err instanceof Error ? err.message : String(err);
14769
14887
  console.error(`[Memory] StateDb initialization failed: ${msg}`);
@@ -14782,6 +14900,15 @@ async function main() {
14782
14900
  initializeLogger2(vaultPath).catch((err) => {
14783
14901
  console.error(`[Memory] Write logger initialization failed: ${err}`);
14784
14902
  });
14903
+ if (isIndexStale(vaultPath)) {
14904
+ buildFTS5Index(vaultPath).then(() => {
14905
+ console.error("[Memory] FTS5 search index ready");
14906
+ }).catch((err) => {
14907
+ console.error("[Memory] FTS5 build failed:", err);
14908
+ });
14909
+ } else {
14910
+ console.error("[Memory] FTS5 search index already fresh, skipping rebuild");
14911
+ }
14785
14912
  let cachedIndex = null;
14786
14913
  if (stateDb) {
14787
14914
  try {
@@ -14879,6 +15006,7 @@ async function updateEntitiesInStateDb() {
14879
15006
  }
14880
15007
  async function runPostIndexWork(index) {
14881
15008
  await updateEntitiesInStateDb();
15009
+ await initializeEntityIndex(vaultPath);
14882
15010
  await exportHubScores(index, stateDb);
14883
15011
  if (stateDb) {
14884
15012
  try {
@@ -14917,6 +15045,33 @@ async function runPostIndexWork(index) {
14917
15045
  if (flywheelConfig.vault_name) {
14918
15046
  console.error(`[Memory] Vault: ${flywheelConfig.vault_name}`);
14919
15047
  }
15048
+ if (hasEmbeddingsIndex()) {
15049
+ console.error("[Memory] Embeddings already built, skipping full scan");
15050
+ } else {
15051
+ setEmbeddingsBuilding(true);
15052
+ buildEmbeddingsIndex(vaultPath, (p) => {
15053
+ if (p.current % 100 === 0 || p.current === p.total) {
15054
+ console.error(`[Semantic] ${p.current}/${p.total}`);
15055
+ }
15056
+ }).then(async () => {
15057
+ if (stateDb) {
15058
+ const entities = getAllEntitiesFromDb2(stateDb);
15059
+ if (entities.length > 0) {
15060
+ const entityMap = new Map(entities.map((e) => [e.name, {
15061
+ name: e.name,
15062
+ path: e.path,
15063
+ category: e.category,
15064
+ aliases: e.aliases
15065
+ }]));
15066
+ await buildEntityEmbeddingsIndex(vaultPath, entityMap);
15067
+ }
15068
+ }
15069
+ loadEntityEmbeddingsToMemory();
15070
+ console.error("[Memory] Embeddings refreshed");
15071
+ }).catch((err) => {
15072
+ console.error("[Memory] Embeddings refresh failed:", err);
15073
+ });
15074
+ }
14920
15075
  if (process.env.FLYWHEEL_WATCH !== "false") {
14921
15076
  const config = parseWatcherConfig();
14922
15077
  console.error(`[Memory] File watcher enabled (debounce: ${config.debounceMs}ms)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.22",
3
+ "version": "2.0.24",
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.22",
53
+ "@velvetmonkey/vault-core": "^2.0.24",
54
54
  "better-sqlite3": "^11.0.0",
55
55
  "chokidar": "^4.0.0",
56
56
  "gray-matter": "^4.0.3",