@velvetmonkey/flywheel-memory 2.0.21 → 2.0.23

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 +131 -14
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -5010,6 +5010,15 @@ function getEntityIndexStats() {
5010
5010
  organizations: entityIndex.organizations?.length ?? 0,
5011
5011
  locations: entityIndex.locations?.length ?? 0,
5012
5012
  concepts: entityIndex.concepts?.length ?? 0,
5013
+ animals: entityIndex.animals?.length ?? 0,
5014
+ media: entityIndex.media?.length ?? 0,
5015
+ events: entityIndex.events?.length ?? 0,
5016
+ documents: entityIndex.documents?.length ?? 0,
5017
+ vehicles: entityIndex.vehicles?.length ?? 0,
5018
+ health: entityIndex.health?.length ?? 0,
5019
+ finance: entityIndex.finance?.length ?? 0,
5020
+ food: entityIndex.food?.length ?? 0,
5021
+ hobbies: entityIndex.hobbies?.length ?? 0,
5013
5022
  other: entityIndex.other.length
5014
5023
  }
5015
5024
  };
@@ -5146,14 +5155,32 @@ var DEFAULT_STRICTNESS = "conservative";
5146
5155
  var TYPE_BOOST = {
5147
5156
  people: 5,
5148
5157
  // Names are high value for connections
5158
+ animals: 3,
5159
+ // Pets and animals are personal and specific
5149
5160
  projects: 3,
5150
5161
  // Projects provide context
5151
5162
  organizations: 2,
5152
5163
  // Companies/teams relevant
5164
+ events: 2,
5165
+ // Meetings, trips, milestones
5166
+ media: 2,
5167
+ // Movies, books, shows
5168
+ health: 2,
5169
+ // Medical, fitness — personal relevance
5170
+ vehicles: 2,
5171
+ // Cars, bikes — specific items
5153
5172
  locations: 1,
5154
5173
  // Geographic context
5155
5174
  concepts: 1,
5156
5175
  // Abstract concepts
5176
+ documents: 1,
5177
+ // Reports, guides
5178
+ food: 1,
5179
+ // Recipes, restaurants
5180
+ hobbies: 1,
5181
+ // Crafts, sports
5182
+ finance: 1,
5183
+ // Accounts, budgets
5157
5184
  technologies: 0,
5158
5185
  // Common, avoid over-suggesting
5159
5186
  acronyms: 0,
@@ -5197,14 +5224,24 @@ var CONTEXT_BOOST = {
5197
5224
  daily: {
5198
5225
  people: 5,
5199
5226
  // Daily notes often mention people
5200
- projects: 2
5227
+ animals: 3,
5228
+ // Pets in daily life
5229
+ events: 3,
5230
+ // Daily events and meetings
5231
+ projects: 2,
5201
5232
  // Work updates reference projects
5233
+ food: 2,
5234
+ // Meals and recipes in daily logs
5235
+ health: 2
5236
+ // Fitness and wellness tracking
5202
5237
  },
5203
5238
  project: {
5204
5239
  projects: 5,
5205
5240
  // Project docs reference other projects
5206
- technologies: 2
5241
+ technologies: 2,
5207
5242
  // Technical dependencies
5243
+ documents: 2
5244
+ // Reference documents
5208
5245
  },
5209
5246
  tech: {
5210
5247
  technologies: 5,
@@ -5722,9 +5759,18 @@ var EXCLUDED_DIRS3 = /* @__PURE__ */ new Set([
5722
5759
  ]);
5723
5760
  var MAX_INDEX_FILE_SIZE = 5 * 1024 * 1024;
5724
5761
  var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
5762
+ function splitFrontmatter(raw) {
5763
+ if (!raw.startsWith("---")) return { frontmatter: "", body: raw };
5764
+ const end = raw.indexOf("\n---", 3);
5765
+ if (end === -1) return { frontmatter: "", body: raw };
5766
+ const yaml = raw.substring(4, end);
5767
+ const values = yaml.split("\n").map((line) => line.replace(/^[\s-]*/, "").replace(/^[\w]+:\s*/, "")).filter((v) => v && !v.startsWith("[") && !v.startsWith("{")).join(" ");
5768
+ return { frontmatter: values, body: raw.substring(end + 4) };
5769
+ }
5725
5770
  var db2 = null;
5726
5771
  var state = {
5727
5772
  ready: false,
5773
+ building: false,
5728
5774
  lastBuilt: null,
5729
5775
  noteCount: 0,
5730
5776
  error: null
@@ -5740,6 +5786,7 @@ function setFTS5Database(database) {
5740
5786
  const countRow = db2.prepare("SELECT COUNT(*) as count FROM notes_fts").get();
5741
5787
  state = {
5742
5788
  ready: countRow.count > 0,
5789
+ building: false,
5743
5790
  lastBuilt,
5744
5791
  noteCount: countRow.count,
5745
5792
  error: null
@@ -5755,6 +5802,7 @@ function shouldIndexFile2(filePath) {
5755
5802
  async function buildFTS5Index(vaultPath2) {
5756
5803
  try {
5757
5804
  state.error = null;
5805
+ state.building = true;
5758
5806
  if (!db2) {
5759
5807
  throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
5760
5808
  }
@@ -5762,7 +5810,7 @@ async function buildFTS5Index(vaultPath2) {
5762
5810
  const files = await scanVault(vaultPath2);
5763
5811
  const indexableFiles = files.filter((f) => shouldIndexFile2(f.path));
5764
5812
  const insert = db2.prepare(
5765
- "INSERT INTO notes_fts (path, title, content) VALUES (?, ?, ?)"
5813
+ "INSERT INTO notes_fts (path, title, frontmatter, content) VALUES (?, ?, ?, ?)"
5766
5814
  );
5767
5815
  const insertMany = db2.transaction((filesToIndex) => {
5768
5816
  let indexed2 = 0;
@@ -5772,9 +5820,10 @@ async function buildFTS5Index(vaultPath2) {
5772
5820
  if (stats.size > MAX_INDEX_FILE_SIZE) {
5773
5821
  continue;
5774
5822
  }
5775
- const content = fs7.readFileSync(file.absolutePath, "utf-8");
5823
+ const raw = fs7.readFileSync(file.absolutePath, "utf-8");
5824
+ const { frontmatter, body } = splitFrontmatter(raw);
5776
5825
  const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
5777
- insert.run(file.path, title, content);
5826
+ insert.run(file.path, title, frontmatter, body);
5778
5827
  indexed2++;
5779
5828
  } catch (err) {
5780
5829
  console.error(`[FTS5] Skipping ${file.path}:`, err);
@@ -5789,6 +5838,7 @@ async function buildFTS5Index(vaultPath2) {
5789
5838
  ).run("last_built", now.toISOString());
5790
5839
  state = {
5791
5840
  ready: true,
5841
+ building: false,
5792
5842
  lastBuilt: now,
5793
5843
  noteCount: indexed,
5794
5844
  error: null
@@ -5798,6 +5848,7 @@ async function buildFTS5Index(vaultPath2) {
5798
5848
  } catch (err) {
5799
5849
  state = {
5800
5850
  ready: false,
5851
+ building: false,
5801
5852
  lastBuilt: null,
5802
5853
  noteCount: 0,
5803
5854
  error: err instanceof Error ? err.message : String(err)
@@ -5832,10 +5883,10 @@ function searchFTS5(_vaultPath, query, limit = 10) {
5832
5883
  SELECT
5833
5884
  path,
5834
5885
  title,
5835
- snippet(notes_fts, 2, '[', ']', '...', 20) as snippet
5886
+ snippet(notes_fts, 3, '<mark>', '</mark>', '...', 20) as snippet
5836
5887
  FROM notes_fts
5837
5888
  WHERE notes_fts MATCH ?
5838
- ORDER BY rank
5889
+ ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
5839
5890
  LIMIT ?
5840
5891
  `);
5841
5892
  const results = stmt.all(query, limit);
@@ -6766,6 +6817,9 @@ function getActivitySummary(index, days) {
6766
6817
  };
6767
6818
  }
6768
6819
 
6820
+ // src/tools/read/health.ts
6821
+ import { SCHEMA_VERSION } from "@velvetmonkey/vault-core";
6822
+
6769
6823
  // src/core/shared/indexActivity.ts
6770
6824
  function recordIndexEvent(stateDb2, event) {
6771
6825
  stateDb2.db.prepare(
@@ -6858,6 +6912,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6858
6912
  });
6859
6913
  const HealthCheckOutputSchema = {
6860
6914
  status: z3.enum(["healthy", "degraded", "unhealthy"]).describe("Overall health status"),
6915
+ schema_version: z3.coerce.number().describe("StateDb schema version"),
6861
6916
  vault_accessible: z3.boolean().describe("Whether the vault path is accessible"),
6862
6917
  vault_path: z3.string().describe("The vault path being used"),
6863
6918
  index_state: z3.enum(["building", "ready", "error"]).describe("Current state of the vault index"),
@@ -6877,6 +6932,10 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6877
6932
  duration_ms: z3.number(),
6878
6933
  ago_seconds: z3.number()
6879
6934
  }).optional().describe("Most recent index rebuild event"),
6935
+ fts5_ready: z3.boolean().describe("Whether the FTS5 keyword search index is ready"),
6936
+ fts5_building: z3.boolean().describe("Whether the FTS5 keyword search index is currently building"),
6937
+ embeddings_ready: z3.boolean().describe("Whether semantic embeddings have been built (enables hybrid keyword+semantic search)"),
6938
+ embeddings_count: z3.coerce.number().describe("Number of notes with semantic embeddings"),
6880
6939
  recommendations: z3.array(z3.string()).describe("Suggested actions if any issues detected")
6881
6940
  };
6882
6941
  server2.registerTool(
@@ -6962,8 +7021,10 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6962
7021
  } catch {
6963
7022
  }
6964
7023
  }
7024
+ const ftsState = getFTS5State();
6965
7025
  const output = {
6966
7026
  status,
7027
+ schema_version: SCHEMA_VERSION,
6967
7028
  vault_accessible: vaultAccessible,
6968
7029
  vault_path: vaultPath2,
6969
7030
  index_state: indexState2,
@@ -6978,6 +7039,10 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6978
7039
  periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
6979
7040
  config: configInfo,
6980
7041
  last_rebuild: lastRebuild,
7042
+ fts5_ready: ftsState.ready,
7043
+ fts5_building: ftsState.building,
7044
+ embeddings_ready: hasEmbeddingsIndex(),
7045
+ embeddings_count: getEmbeddingsCount(),
6981
7046
  recommendations
6982
7047
  };
6983
7048
  return {
@@ -7320,27 +7385,55 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
7320
7385
  return { content: [{ type: "text", text: JSON.stringify({ error: "query is required for content search" }, null, 2) }] };
7321
7386
  }
7322
7387
  const ftsState = getFTS5State();
7388
+ if (ftsState.building) {
7389
+ return { content: [{ type: "text", text: JSON.stringify({
7390
+ scope,
7391
+ method: "fts5",
7392
+ query,
7393
+ building: true,
7394
+ total_results: 0,
7395
+ results: [],
7396
+ message: "Search index is building, try again shortly"
7397
+ }, null, 2) }] };
7398
+ }
7323
7399
  if (!ftsState.ready || isIndexStale(vaultPath2)) {
7324
7400
  console.error("[FTS5] Index stale or missing, rebuilding...");
7325
7401
  await buildFTS5Index(vaultPath2);
7326
7402
  }
7327
7403
  const fts5Results = searchFTS5(vaultPath2, query, limit);
7404
+ let entityResults = [];
7405
+ if (scope === "all") {
7406
+ const stateDb2 = getStateDb();
7407
+ if (stateDb2) {
7408
+ try {
7409
+ entityResults = searchEntities(stateDb2, query, limit);
7410
+ } catch {
7411
+ }
7412
+ }
7413
+ }
7328
7414
  if (hasEmbeddingsIndex()) {
7329
7415
  try {
7330
7416
  const semanticResults = await semanticSearch(query, limit);
7331
7417
  const fts5Ranked = fts5Results.map((r) => ({ path: r.path, title: r.title, snippet: r.snippet }));
7332
7418
  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)]);
7419
+ const entityRanked = entityResults.map((r) => ({ path: r.path, title: r.name }));
7420
+ const rrfScores = reciprocalRankFusion(fts5Ranked, semanticRanked, entityRanked);
7421
+ const allPaths = /* @__PURE__ */ new Set([
7422
+ ...fts5Results.map((r) => r.path),
7423
+ ...semanticResults.map((r) => r.path),
7424
+ ...entityResults.map((r) => r.path)
7425
+ ]);
7335
7426
  const fts5Map = new Map(fts5Results.map((r) => [r.path, r]));
7336
7427
  const semanticMap = new Map(semanticResults.map((r) => [r.path, r]));
7428
+ const entityMap = new Map(entityResults.map((r) => [r.path, r]));
7337
7429
  const merged = Array.from(allPaths).map((p) => ({
7338
7430
  path: p,
7339
- title: fts5Map.get(p)?.title || semanticMap.get(p)?.title || p.replace(/\.md$/, "").split("/").pop() || p,
7431
+ title: fts5Map.get(p)?.title || semanticMap.get(p)?.title || entityMap.get(p)?.name || p.replace(/\.md$/, "").split("/").pop() || p,
7340
7432
  snippet: fts5Map.get(p)?.snippet,
7341
7433
  rrf_score: Math.round((rrfScores.get(p) || 0) * 1e4) / 1e4,
7342
7434
  in_fts5: fts5Map.has(p),
7343
- in_semantic: semanticMap.has(p)
7435
+ in_semantic: semanticMap.has(p),
7436
+ in_entity: entityMap.has(p)
7344
7437
  }));
7345
7438
  merged.sort((a, b) => b.rrf_score - a.rrf_score);
7346
7439
  return { content: [{ type: "text", text: JSON.stringify({
@@ -7354,6 +7447,21 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
7354
7447
  console.error("[Semantic] Hybrid search failed, falling back to FTS5:", err instanceof Error ? err.message : err);
7355
7448
  }
7356
7449
  }
7450
+ if (entityResults.length > 0) {
7451
+ const fts5Map = new Map(fts5Results.map((r) => [r.path, r]));
7452
+ const entityRanked = entityResults.filter((r) => !fts5Map.has(r.path));
7453
+ const merged = [
7454
+ ...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) })),
7455
+ ...entityRanked.map((r) => ({ path: r.path, title: r.name, snippet: void 0, in_entity: true }))
7456
+ ];
7457
+ return { content: [{ type: "text", text: JSON.stringify({
7458
+ scope: "content",
7459
+ method: "fts5",
7460
+ query,
7461
+ total_results: merged.length,
7462
+ results: merged.slice(0, limit)
7463
+ }, null, 2) }] };
7464
+ }
7357
7465
  return { content: [{ type: "text", text: JSON.stringify({
7358
7466
  scope: "content",
7359
7467
  method: "fts5",
@@ -14147,11 +14255,11 @@ function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14147
14255
  SELECT
14148
14256
  path,
14149
14257
  title,
14150
- bm25(notes_fts) as score,
14151
- snippet(notes_fts, 2, '[', ']', '...', 15) as snippet
14258
+ bm25(notes_fts, 0.0, 5.0, 10.0, 1.0) as score,
14259
+ snippet(notes_fts, 3, '[', ']', '...', 15) as snippet
14152
14260
  FROM notes_fts
14153
14261
  WHERE notes_fts MATCH ?
14154
- ORDER BY rank
14262
+ ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
14155
14263
  LIMIT ?
14156
14264
  `).all(query, limit + 20);
14157
14265
  let filtered = results.filter((r) => r.path !== sourcePath);
@@ -14908,6 +15016,15 @@ async function runPostIndexWork(index) {
14908
15016
  console.error("[Memory] Failed to update suppression list:", err);
14909
15017
  }
14910
15018
  }
15019
+ if (isIndexStale(vaultPath)) {
15020
+ buildFTS5Index(vaultPath).then(() => {
15021
+ console.error("[Memory] FTS5 search index ready");
15022
+ }).catch((err) => {
15023
+ console.error("[Memory] FTS5 build failed:", err);
15024
+ });
15025
+ } else {
15026
+ console.error("[Memory] FTS5 search index already fresh, skipping rebuild");
15027
+ }
14911
15028
  const existing = loadConfig(stateDb);
14912
15029
  const inferred = inferConfig(index, vaultPath);
14913
15030
  if (stateDb) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.21",
3
+ "version": "2.0.23",
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.21",
53
+ "@velvetmonkey/vault-core": "^2.0.23",
54
54
  "better-sqlite3": "^11.0.0",
55
55
  "chokidar": "^4.0.0",
56
56
  "gray-matter": "^4.0.3",