@velvetmonkey/flywheel-memory 2.0.157 → 2.1.0
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 +133 -47
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -557,6 +557,18 @@ function hasEmbeddingsIndex() {
|
|
|
557
557
|
const row = db4.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
|
|
558
558
|
return row.count > 0;
|
|
559
559
|
}
|
|
560
|
+
if (!isEmbeddingsBuilding()) {
|
|
561
|
+
const row = db4.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
|
|
562
|
+
if (row.count > 0) {
|
|
563
|
+
setEmbeddingsBuildState("complete");
|
|
564
|
+
console.error("[Semantic] Recovered stale embeddings_state \u2192 complete");
|
|
565
|
+
return true;
|
|
566
|
+
} else {
|
|
567
|
+
setEmbeddingsBuildState("none");
|
|
568
|
+
console.error("[Semantic] Recovered stale embeddings_state \u2192 none (no rows)");
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
560
572
|
return false;
|
|
561
573
|
} catch {
|
|
562
574
|
return false;
|
|
@@ -665,8 +677,13 @@ function diagnoseEmbeddings(vaultPath2) {
|
|
|
665
677
|
} else {
|
|
666
678
|
checks.push({ name: "entity_orphans", status: "ok", detail: "0 orphaned" });
|
|
667
679
|
}
|
|
668
|
-
} catch {
|
|
669
|
-
|
|
680
|
+
} catch (e) {
|
|
681
|
+
const msg = String(e);
|
|
682
|
+
if (msg.includes("no such table")) {
|
|
683
|
+
checks.push({ name: "entity_orphans", status: "ok", detail: "No entity embeddings table" });
|
|
684
|
+
} else {
|
|
685
|
+
checks.push({ name: "entity_orphans", status: "warning", detail: "Could not check entity orphans" });
|
|
686
|
+
}
|
|
670
687
|
}
|
|
671
688
|
try {
|
|
672
689
|
const samples = db4.prepare("SELECT embedding FROM note_embeddings ORDER BY RANDOM() LIMIT 3").all();
|
|
@@ -7701,6 +7718,38 @@ function purgeOldNoteLinkHistory(stateDb2, retentionDays = 90) {
|
|
|
7701
7718
|
return result.changes;
|
|
7702
7719
|
}
|
|
7703
7720
|
|
|
7721
|
+
// src/core/read/identity.ts
|
|
7722
|
+
function normalizeResolvedPath(notePath) {
|
|
7723
|
+
return notePath.toLowerCase().replace(/\.md$/, "");
|
|
7724
|
+
}
|
|
7725
|
+
function getInboundTargetsForNote(stateDb2, notePath) {
|
|
7726
|
+
const targets = /* @__PURE__ */ new Set();
|
|
7727
|
+
const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase();
|
|
7728
|
+
const normalizedPath = normalizeResolvedPath(notePath);
|
|
7729
|
+
if (stateDb2) {
|
|
7730
|
+
try {
|
|
7731
|
+
const row = stateDb2.db.prepare(
|
|
7732
|
+
`SELECT name_lower, aliases_json FROM entities
|
|
7733
|
+
WHERE LOWER(REPLACE(path, '.md', '')) = ?`
|
|
7734
|
+
).get(normalizedPath);
|
|
7735
|
+
if (row) {
|
|
7736
|
+
targets.add(row.name_lower);
|
|
7737
|
+
if (row.aliases_json) {
|
|
7738
|
+
try {
|
|
7739
|
+
for (const alias of JSON.parse(row.aliases_json)) {
|
|
7740
|
+
targets.add(alias.toLowerCase());
|
|
7741
|
+
}
|
|
7742
|
+
} catch {
|
|
7743
|
+
}
|
|
7744
|
+
}
|
|
7745
|
+
}
|
|
7746
|
+
} catch {
|
|
7747
|
+
}
|
|
7748
|
+
}
|
|
7749
|
+
if (stem2) targets.add(stem2);
|
|
7750
|
+
return [...targets];
|
|
7751
|
+
}
|
|
7752
|
+
|
|
7704
7753
|
// src/core/shared/hubExport.ts
|
|
7705
7754
|
var EIGEN_ITERATIONS = 50;
|
|
7706
7755
|
function computeHubScores(index) {
|
|
@@ -7784,15 +7833,16 @@ function computeHubScores(index) {
|
|
|
7784
7833
|
}
|
|
7785
7834
|
return { scores: result, edgeCount };
|
|
7786
7835
|
}
|
|
7787
|
-
function updateHubScoresInDb(stateDb2, hubScores) {
|
|
7788
|
-
const
|
|
7789
|
-
|
|
7790
|
-
`);
|
|
7836
|
+
function updateHubScoresInDb(stateDb2, hubScores, pathToId) {
|
|
7837
|
+
const resetAll = stateDb2.db.prepare("UPDATE entities SET hub_score = 0");
|
|
7838
|
+
const updateById = stateDb2.db.prepare("UPDATE entities SET hub_score = ? WHERE id = ?");
|
|
7791
7839
|
let updated = 0;
|
|
7792
7840
|
const transaction = stateDb2.db.transaction(() => {
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7841
|
+
resetAll.run();
|
|
7842
|
+
for (const [normalizedPath, score] of hubScores) {
|
|
7843
|
+
const entityId = pathToId.get(normalizedPath);
|
|
7844
|
+
if (entityId !== void 0) {
|
|
7845
|
+
updateById.run(score, entityId);
|
|
7796
7846
|
updated++;
|
|
7797
7847
|
}
|
|
7798
7848
|
}
|
|
@@ -7807,9 +7857,14 @@ async function exportHubScores(vaultIndex2, stateDb2) {
|
|
|
7807
7857
|
}
|
|
7808
7858
|
const { scores: hubScores, edgeCount } = computeHubScores(vaultIndex2);
|
|
7809
7859
|
console.error(`[Flywheel] Computed hub scores for ${hubScores.size} notes (${edgeCount} edges in graph)`);
|
|
7860
|
+
const entityRows = stateDb2.db.prepare("SELECT id, path FROM entities").all();
|
|
7861
|
+
const pathToId = /* @__PURE__ */ new Map();
|
|
7862
|
+
for (const row of entityRows) {
|
|
7863
|
+
pathToId.set(normalizeResolvedPath(row.path), row.id);
|
|
7864
|
+
}
|
|
7810
7865
|
try {
|
|
7811
|
-
const updated = updateHubScoresInDb(stateDb2, hubScores);
|
|
7812
|
-
console.error(`[Flywheel]
|
|
7866
|
+
const updated = updateHubScoresInDb(stateDb2, hubScores, pathToId);
|
|
7867
|
+
console.error(`[Flywheel] Hub scores: ${updated}/${hubScores.size} scores mapped to entities (all others reset to 0)`);
|
|
7813
7868
|
return updated;
|
|
7814
7869
|
} catch (e) {
|
|
7815
7870
|
console.error("[Flywheel] Failed to update hub scores in StateDb:", e);
|
|
@@ -8752,7 +8807,12 @@ var PipelineRunner = class {
|
|
|
8752
8807
|
} catch {
|
|
8753
8808
|
}
|
|
8754
8809
|
}
|
|
8755
|
-
|
|
8810
|
+
let orphansRemoved = 0;
|
|
8811
|
+
try {
|
|
8812
|
+
orphansRemoved = removeOrphanedNoteEmbeddings();
|
|
8813
|
+
} catch (e) {
|
|
8814
|
+
serverLog("watcher", `Note embedding orphan cleanup failed: ${e}`, "error");
|
|
8815
|
+
}
|
|
8756
8816
|
tracker.end({ updated: embUpdated, removed: embRemoved, orphans_removed: orphansRemoved });
|
|
8757
8817
|
serverLog("watcher", `Note embeddings: ${embUpdated} updated, ${embRemoved} removed, ${orphansRemoved} orphans cleaned`);
|
|
8758
8818
|
} else {
|
|
@@ -8785,7 +8845,8 @@ var PipelineRunner = class {
|
|
|
8785
8845
|
}
|
|
8786
8846
|
const currentNames = new Set(allEntities.map((e) => e.name));
|
|
8787
8847
|
entEmbOrphansRemoved = removeOrphanedEntityEmbeddings(currentNames);
|
|
8788
|
-
} catch {
|
|
8848
|
+
} catch (e) {
|
|
8849
|
+
serverLog("watcher", `Entity embedding update/orphan cleanup failed: ${e}`, "error");
|
|
8789
8850
|
}
|
|
8790
8851
|
tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10), orphans_removed: entEmbOrphansRemoved });
|
|
8791
8852
|
serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated, ${entEmbOrphansRemoved} orphans cleaned`);
|
|
@@ -9336,10 +9397,15 @@ var PipelineRunner = class {
|
|
|
9336
9397
|
return { skipped: true, reason: "vacuumed recently" };
|
|
9337
9398
|
}
|
|
9338
9399
|
p.sd.db.pragma("incremental_vacuum");
|
|
9339
|
-
p.sd.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9400
|
+
const walResult = p.sd.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
9401
|
+
const checkpointed = walResult?.[0]?.busy === 0;
|
|
9402
|
+
if (checkpointed) {
|
|
9403
|
+
p.sd.setMetadataValue.run("last_incremental_vacuum", String(Date.now()));
|
|
9404
|
+
serverLog("watcher", "Incremental vacuum + WAL checkpoint completed");
|
|
9405
|
+
} else {
|
|
9406
|
+
serverLog("watcher", "Incremental vacuum done, WAL checkpoint skipped (busy readers)");
|
|
9407
|
+
}
|
|
9408
|
+
return { vacuumed: true, wal_checkpointed: checkpointed };
|
|
9343
9409
|
}
|
|
9344
9410
|
};
|
|
9345
9411
|
|
|
@@ -9842,11 +9908,9 @@ function computeMetrics(index, stateDb2) {
|
|
|
9842
9908
|
for (const bl of backlinks) {
|
|
9843
9909
|
connectedNotes.add(bl.source);
|
|
9844
9910
|
}
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
connectedNotes.add(note.path);
|
|
9849
|
-
}
|
|
9911
|
+
const targetPath = index.entities.get(target);
|
|
9912
|
+
if (targetPath && index.notes.has(targetPath)) {
|
|
9913
|
+
connectedNotes.add(targetPath);
|
|
9850
9914
|
}
|
|
9851
9915
|
}
|
|
9852
9916
|
let orphanCount = 0;
|
|
@@ -10878,14 +10942,18 @@ function rankOutlinks(outlinks, notePath, index, stateDb2, maxLinks = TOP_LINKS)
|
|
|
10878
10942
|
}).sort((a, b) => (b.weight ?? 1) - (a.weight ?? 1)).slice(0, maxLinks);
|
|
10879
10943
|
}
|
|
10880
10944
|
function rankBacklinks(backlinks, notePath, index, stateDb2, maxLinks = TOP_LINKS) {
|
|
10881
|
-
const
|
|
10945
|
+
const targets = getInboundTargetsForNote(stateDb2, notePath);
|
|
10882
10946
|
const weightMap = /* @__PURE__ */ new Map();
|
|
10883
|
-
if (stateDb2 &&
|
|
10947
|
+
if (stateDb2 && targets.length > 0) {
|
|
10884
10948
|
try {
|
|
10949
|
+
const placeholders = targets.map(() => "?").join(",");
|
|
10885
10950
|
const rows = stateDb2.db.prepare(
|
|
10886
|
-
|
|
10887
|
-
).all(
|
|
10888
|
-
for (const row of rows)
|
|
10951
|
+
`SELECT note_path, weight FROM note_links WHERE target IN (${placeholders})`
|
|
10952
|
+
).all(...targets);
|
|
10953
|
+
for (const row of rows) {
|
|
10954
|
+
const existing = weightMap.get(row.note_path) ?? 0;
|
|
10955
|
+
weightMap.set(row.note_path, Math.max(existing, row.weight));
|
|
10956
|
+
}
|
|
10889
10957
|
} catch {
|
|
10890
10958
|
}
|
|
10891
10959
|
}
|
|
@@ -12757,16 +12825,18 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb3) {
|
|
|
12757
12825
|
return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
|
|
12758
12826
|
}
|
|
12759
12827
|
const limit = Math.min(requestedLimit ?? 20, MAX_LIMIT);
|
|
12760
|
-
const
|
|
12828
|
+
const targets = getInboundTargetsForNote(stateDb2, notePath);
|
|
12829
|
+
const inPlaceholders = targets.map(() => "?").join(",");
|
|
12761
12830
|
const rows = stateDb2.db.prepare(`
|
|
12762
12831
|
SELECT target AS node, weight, 'outgoing' AS direction
|
|
12763
12832
|
FROM note_links WHERE note_path = ?
|
|
12764
12833
|
UNION ALL
|
|
12765
|
-
SELECT note_path AS node, weight, 'incoming' AS direction
|
|
12766
|
-
FROM note_links WHERE target
|
|
12834
|
+
SELECT note_path AS node, MAX(weight) AS weight, 'incoming' AS direction
|
|
12835
|
+
FROM note_links WHERE target IN (${inPlaceholders})
|
|
12836
|
+
GROUP BY note_path
|
|
12767
12837
|
ORDER BY weight DESC
|
|
12768
12838
|
LIMIT ?
|
|
12769
|
-
`).all(notePath,
|
|
12839
|
+
`).all(notePath, ...targets, limit);
|
|
12770
12840
|
return {
|
|
12771
12841
|
content: [{
|
|
12772
12842
|
type: "text",
|
|
@@ -22974,6 +23044,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb3) {
|
|
|
22974
23044
|
if (!force) {
|
|
22975
23045
|
const diagnosis2 = diagnoseEmbeddings(vaultPath2);
|
|
22976
23046
|
if (diagnosis2.healthy) {
|
|
23047
|
+
setEmbeddingsBuildState("complete");
|
|
22977
23048
|
return {
|
|
22978
23049
|
content: [{
|
|
22979
23050
|
type: "text",
|
|
@@ -24986,19 +25057,8 @@ function registerAllTools(targetServer, ctx) {
|
|
|
24986
25057
|
var __filename = fileURLToPath2(import.meta.url);
|
|
24987
25058
|
var __dirname = dirname7(__filename);
|
|
24988
25059
|
var pkg = JSON.parse(readFileSync6(join21(__dirname, "../package.json"), "utf-8"));
|
|
24989
|
-
var
|
|
24990
|
-
var vaultPath = _earlyVaultConfigs ? _earlyVaultConfigs[0].path : process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
25060
|
+
var vaultPath;
|
|
24991
25061
|
var resolvedVaultPath;
|
|
24992
|
-
try {
|
|
24993
|
-
resolvedVaultPath = realpathSync(vaultPath).replace(/\\/g, "/");
|
|
24994
|
-
} catch {
|
|
24995
|
-
resolvedVaultPath = vaultPath.replace(/\\/g, "/");
|
|
24996
|
-
}
|
|
24997
|
-
if (!existsSync3(resolvedVaultPath)) {
|
|
24998
|
-
console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
|
|
24999
|
-
console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
|
|
25000
|
-
process.exit(1);
|
|
25001
|
-
}
|
|
25002
25062
|
var vaultIndex;
|
|
25003
25063
|
var flywheelConfig = {};
|
|
25004
25064
|
var stateDb = null;
|
|
@@ -25262,10 +25322,21 @@ async function bootVault(ctx, startTime) {
|
|
|
25262
25322
|
}
|
|
25263
25323
|
}
|
|
25264
25324
|
async function main() {
|
|
25325
|
+
const vaultConfigs = parseVaultConfig();
|
|
25326
|
+
vaultPath = vaultConfigs ? vaultConfigs[0].path : process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
25327
|
+
try {
|
|
25328
|
+
resolvedVaultPath = realpathSync(vaultPath).replace(/\\/g, "/");
|
|
25329
|
+
} catch {
|
|
25330
|
+
resolvedVaultPath = vaultPath.replace(/\\/g, "/");
|
|
25331
|
+
}
|
|
25332
|
+
if (!existsSync3(resolvedVaultPath)) {
|
|
25333
|
+
console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
|
|
25334
|
+
console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
|
|
25335
|
+
process.exit(1);
|
|
25336
|
+
}
|
|
25265
25337
|
serverLog("server", `Starting Flywheel Memory v${pkg.version}...`);
|
|
25266
25338
|
serverLog("server", `Vault: ${vaultPath}`);
|
|
25267
25339
|
const startTime = Date.now();
|
|
25268
|
-
const vaultConfigs = _earlyVaultConfigs;
|
|
25269
25340
|
if (vaultConfigs) {
|
|
25270
25341
|
vaultRegistry = new VaultRegistry(vaultConfigs[0].name);
|
|
25271
25342
|
serverLog("server", `Multi-vault mode: ${vaultConfigs.map((v) => v.name).join(", ")}`);
|
|
@@ -25752,17 +25823,19 @@ async function runPostIndexWork(ctx) {
|
|
|
25752
25823
|
}
|
|
25753
25824
|
if (process.argv.includes("--init-semantic")) {
|
|
25754
25825
|
(async () => {
|
|
25826
|
+
const semanticVaultConfigs = parseVaultConfig();
|
|
25827
|
+
const semanticVaultPath = semanticVaultConfigs ? semanticVaultConfigs[0].path : process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
25755
25828
|
console.error("[Semantic] Pre-warming semantic search...");
|
|
25756
|
-
console.error(`[Semantic] Vault: ${
|
|
25829
|
+
console.error(`[Semantic] Vault: ${semanticVaultPath}`);
|
|
25757
25830
|
try {
|
|
25758
|
-
const db4 = openStateDb(
|
|
25831
|
+
const db4 = openStateDb(semanticVaultPath);
|
|
25759
25832
|
setEmbeddingsDatabase(db4.db);
|
|
25760
25833
|
const storedModel = getStoredEmbeddingModel();
|
|
25761
25834
|
if (storedModel && storedModel !== getActiveModelId()) {
|
|
25762
25835
|
console.error(`[Semantic] Model changed ${storedModel} \u2192 ${getActiveModelId()}, clearing`);
|
|
25763
25836
|
clearEmbeddingsForRebuild();
|
|
25764
25837
|
}
|
|
25765
|
-
const progress = await buildEmbeddingsIndex(
|
|
25838
|
+
const progress = await buildEmbeddingsIndex(semanticVaultPath, (p) => {
|
|
25766
25839
|
if (p.current % 50 === 0 || p.current === p.total) {
|
|
25767
25840
|
console.error(`[Semantic] Embedding ${p.current}/${p.total} notes (${p.skipped} skipped)...`);
|
|
25768
25841
|
}
|
|
@@ -25782,6 +25855,19 @@ if (process.argv.includes("--init-semantic")) {
|
|
|
25782
25855
|
process.exit(1);
|
|
25783
25856
|
});
|
|
25784
25857
|
}
|
|
25858
|
+
function gracefulShutdown(signal) {
|
|
25859
|
+
console.error(`[Memory] Received ${signal}, shutting down...`);
|
|
25860
|
+
try {
|
|
25861
|
+
watcherInstance?.stop();
|
|
25862
|
+
} catch {
|
|
25863
|
+
}
|
|
25864
|
+
stopSweepTimer();
|
|
25865
|
+
flushLogs().catch(() => {
|
|
25866
|
+
}).finally(() => process.exit(0));
|
|
25867
|
+
setTimeout(() => process.exit(0), 2e3).unref();
|
|
25868
|
+
}
|
|
25869
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
25870
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
25785
25871
|
process.on("beforeExit", async () => {
|
|
25786
25872
|
stopSweepTimer();
|
|
25787
25873
|
await flushLogs();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "MCP tools that search, write, and auto-link your Obsidian vault — and learn from your edits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@huggingface/transformers": "^3.8.1",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
57
|
-
"@velvetmonkey/vault-core": "^2.0
|
|
57
|
+
"@velvetmonkey/vault-core": "^2.1.0",
|
|
58
58
|
"better-sqlite3": "^12.0.0",
|
|
59
59
|
"chokidar": "^4.0.0",
|
|
60
60
|
"gray-matter": "^4.0.3",
|