@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.
- package/dist/index.js +131 -14
- 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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
5886
|
+
snippet(notes_fts, 3, '<mark>', '</mark>', '...', 20) as snippet
|
|
5836
5887
|
FROM notes_fts
|
|
5837
5888
|
WHERE notes_fts MATCH ?
|
|
5838
|
-
ORDER BY
|
|
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
|
|
7334
|
-
const
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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",
|