@velvetmonkey/flywheel-memory 2.0.44 → 2.0.46
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 +284 -115
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -836,8 +836,8 @@ function createContext(variables = {}) {
|
|
|
836
836
|
steps: {}
|
|
837
837
|
};
|
|
838
838
|
}
|
|
839
|
-
function resolvePath(obj,
|
|
840
|
-
const parts =
|
|
839
|
+
function resolvePath(obj, path33) {
|
|
840
|
+
const parts = path33.split(".");
|
|
841
841
|
let current = obj;
|
|
842
842
|
for (const part of parts) {
|
|
843
843
|
if (current === void 0 || current === null) {
|
|
@@ -1552,10 +1552,10 @@ var init_taskHelpers = __esm({
|
|
|
1552
1552
|
});
|
|
1553
1553
|
|
|
1554
1554
|
// src/index.ts
|
|
1555
|
-
import * as
|
|
1555
|
+
import * as path32 from "path";
|
|
1556
1556
|
import { readFileSync as readFileSync4, realpathSync } from "fs";
|
|
1557
1557
|
import { fileURLToPath } from "url";
|
|
1558
|
-
import { dirname as dirname4, join as
|
|
1558
|
+
import { dirname as dirname4, join as join17 } from "path";
|
|
1559
1559
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1560
1560
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1561
1561
|
|
|
@@ -2216,8 +2216,8 @@ function updateIndexProgress(parsed, total) {
|
|
|
2216
2216
|
function normalizeTarget(target) {
|
|
2217
2217
|
return target.toLowerCase().replace(/\.md$/, "");
|
|
2218
2218
|
}
|
|
2219
|
-
function normalizeNotePath(
|
|
2220
|
-
return
|
|
2219
|
+
function normalizeNotePath(path33) {
|
|
2220
|
+
return path33.toLowerCase().replace(/\.md$/, "");
|
|
2221
2221
|
}
|
|
2222
2222
|
async function buildVaultIndex(vaultPath2, options = {}) {
|
|
2223
2223
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
|
|
@@ -2386,7 +2386,7 @@ function findSimilarEntity(index, target) {
|
|
|
2386
2386
|
}
|
|
2387
2387
|
const maxDist = normalizedLen <= 10 ? 1 : 2;
|
|
2388
2388
|
let bestMatch;
|
|
2389
|
-
for (const [entity,
|
|
2389
|
+
for (const [entity, path33] of index.entities) {
|
|
2390
2390
|
const lenDiff = Math.abs(entity.length - normalizedLen);
|
|
2391
2391
|
if (lenDiff > maxDist) {
|
|
2392
2392
|
continue;
|
|
@@ -2394,7 +2394,7 @@ function findSimilarEntity(index, target) {
|
|
|
2394
2394
|
const dist = levenshteinDistance(normalized, entity);
|
|
2395
2395
|
if (dist > 0 && dist <= maxDist) {
|
|
2396
2396
|
if (!bestMatch || dist < bestMatch.distance) {
|
|
2397
|
-
bestMatch = { path:
|
|
2397
|
+
bestMatch = { path: path33, entity, distance: dist };
|
|
2398
2398
|
if (dist === 1) {
|
|
2399
2399
|
return bestMatch;
|
|
2400
2400
|
}
|
|
@@ -2783,8 +2783,8 @@ function normalizePath(filePath) {
|
|
|
2783
2783
|
return normalized;
|
|
2784
2784
|
}
|
|
2785
2785
|
function getRelativePath(vaultPath2, filePath) {
|
|
2786
|
-
const
|
|
2787
|
-
return normalizePath(
|
|
2786
|
+
const relative3 = path5.relative(vaultPath2, filePath);
|
|
2787
|
+
return normalizePath(relative3);
|
|
2788
2788
|
}
|
|
2789
2789
|
function shouldWatch(filePath, vaultPath2) {
|
|
2790
2790
|
const normalized = normalizePath(filePath);
|
|
@@ -2913,30 +2913,30 @@ var EventQueue = class {
|
|
|
2913
2913
|
* Add a new event to the queue
|
|
2914
2914
|
*/
|
|
2915
2915
|
push(type, rawPath) {
|
|
2916
|
-
const
|
|
2916
|
+
const path33 = normalizePath(rawPath);
|
|
2917
2917
|
const now = Date.now();
|
|
2918
2918
|
const event = {
|
|
2919
2919
|
type,
|
|
2920
|
-
path:
|
|
2920
|
+
path: path33,
|
|
2921
2921
|
timestamp: now
|
|
2922
2922
|
};
|
|
2923
|
-
let pending = this.pending.get(
|
|
2923
|
+
let pending = this.pending.get(path33);
|
|
2924
2924
|
if (!pending) {
|
|
2925
2925
|
pending = {
|
|
2926
2926
|
events: [],
|
|
2927
2927
|
timer: null,
|
|
2928
2928
|
lastEvent: now
|
|
2929
2929
|
};
|
|
2930
|
-
this.pending.set(
|
|
2930
|
+
this.pending.set(path33, pending);
|
|
2931
2931
|
}
|
|
2932
2932
|
pending.events.push(event);
|
|
2933
2933
|
pending.lastEvent = now;
|
|
2934
|
-
console.error(`[flywheel] QUEUE: pushed ${type} for ${
|
|
2934
|
+
console.error(`[flywheel] QUEUE: pushed ${type} for ${path33}, pending=${this.pending.size}`);
|
|
2935
2935
|
if (pending.timer) {
|
|
2936
2936
|
clearTimeout(pending.timer);
|
|
2937
2937
|
}
|
|
2938
2938
|
pending.timer = setTimeout(() => {
|
|
2939
|
-
this.flushPath(
|
|
2939
|
+
this.flushPath(path33);
|
|
2940
2940
|
}, this.config.debounceMs);
|
|
2941
2941
|
if (this.pending.size >= this.config.batchSize) {
|
|
2942
2942
|
this.flush();
|
|
@@ -2957,10 +2957,10 @@ var EventQueue = class {
|
|
|
2957
2957
|
/**
|
|
2958
2958
|
* Flush a single path's events
|
|
2959
2959
|
*/
|
|
2960
|
-
flushPath(
|
|
2961
|
-
const pending = this.pending.get(
|
|
2960
|
+
flushPath(path33) {
|
|
2961
|
+
const pending = this.pending.get(path33);
|
|
2962
2962
|
if (!pending || pending.events.length === 0) return;
|
|
2963
|
-
console.error(`[flywheel] QUEUE: flushing ${
|
|
2963
|
+
console.error(`[flywheel] QUEUE: flushing ${path33}, events=${pending.events.length}`);
|
|
2964
2964
|
if (pending.timer) {
|
|
2965
2965
|
clearTimeout(pending.timer);
|
|
2966
2966
|
pending.timer = null;
|
|
@@ -2969,7 +2969,7 @@ var EventQueue = class {
|
|
|
2969
2969
|
if (coalescedType) {
|
|
2970
2970
|
const coalesced = {
|
|
2971
2971
|
type: coalescedType,
|
|
2972
|
-
path:
|
|
2972
|
+
path: path33,
|
|
2973
2973
|
originalEvents: [...pending.events]
|
|
2974
2974
|
};
|
|
2975
2975
|
this.onBatch({
|
|
@@ -2978,7 +2978,7 @@ var EventQueue = class {
|
|
|
2978
2978
|
timestamp: Date.now()
|
|
2979
2979
|
});
|
|
2980
2980
|
}
|
|
2981
|
-
this.pending.delete(
|
|
2981
|
+
this.pending.delete(path33);
|
|
2982
2982
|
}
|
|
2983
2983
|
/**
|
|
2984
2984
|
* Flush all pending events
|
|
@@ -2990,7 +2990,7 @@ var EventQueue = class {
|
|
|
2990
2990
|
}
|
|
2991
2991
|
if (this.pending.size === 0) return;
|
|
2992
2992
|
const events = [];
|
|
2993
|
-
for (const [
|
|
2993
|
+
for (const [path33, pending] of this.pending) {
|
|
2994
2994
|
if (pending.timer) {
|
|
2995
2995
|
clearTimeout(pending.timer);
|
|
2996
2996
|
}
|
|
@@ -2998,7 +2998,7 @@ var EventQueue = class {
|
|
|
2998
2998
|
if (coalescedType) {
|
|
2999
2999
|
events.push({
|
|
3000
3000
|
type: coalescedType,
|
|
3001
|
-
path:
|
|
3001
|
+
path: path33,
|
|
3002
3002
|
originalEvents: [...pending.events]
|
|
3003
3003
|
});
|
|
3004
3004
|
}
|
|
@@ -3168,8 +3168,8 @@ async function upsertNote(index, vaultPath2, notePath) {
|
|
|
3168
3168
|
removeNoteFromIndex(index, notePath);
|
|
3169
3169
|
}
|
|
3170
3170
|
const fullPath = path7.join(vaultPath2, notePath);
|
|
3171
|
-
const
|
|
3172
|
-
const stats = await
|
|
3171
|
+
const fs32 = await import("fs/promises");
|
|
3172
|
+
const stats = await fs32.stat(fullPath);
|
|
3173
3173
|
const vaultFile = {
|
|
3174
3174
|
path: notePath,
|
|
3175
3175
|
absolutePath: fullPath,
|
|
@@ -3351,31 +3351,31 @@ function createVaultWatcher(options) {
|
|
|
3351
3351
|
usePolling: config.usePolling,
|
|
3352
3352
|
interval: config.usePolling ? config.pollInterval : void 0
|
|
3353
3353
|
});
|
|
3354
|
-
watcher.on("add", (
|
|
3355
|
-
console.error(`[flywheel] RAW EVENT: add ${
|
|
3356
|
-
if (shouldWatch(
|
|
3357
|
-
console.error(`[flywheel] ACCEPTED: add ${
|
|
3358
|
-
eventQueue.push("add",
|
|
3354
|
+
watcher.on("add", (path33) => {
|
|
3355
|
+
console.error(`[flywheel] RAW EVENT: add ${path33}`);
|
|
3356
|
+
if (shouldWatch(path33, vaultPath2)) {
|
|
3357
|
+
console.error(`[flywheel] ACCEPTED: add ${path33}`);
|
|
3358
|
+
eventQueue.push("add", path33);
|
|
3359
3359
|
} else {
|
|
3360
|
-
console.error(`[flywheel] FILTERED: add ${
|
|
3360
|
+
console.error(`[flywheel] FILTERED: add ${path33}`);
|
|
3361
3361
|
}
|
|
3362
3362
|
});
|
|
3363
|
-
watcher.on("change", (
|
|
3364
|
-
console.error(`[flywheel] RAW EVENT: change ${
|
|
3365
|
-
if (shouldWatch(
|
|
3366
|
-
console.error(`[flywheel] ACCEPTED: change ${
|
|
3367
|
-
eventQueue.push("change",
|
|
3363
|
+
watcher.on("change", (path33) => {
|
|
3364
|
+
console.error(`[flywheel] RAW EVENT: change ${path33}`);
|
|
3365
|
+
if (shouldWatch(path33, vaultPath2)) {
|
|
3366
|
+
console.error(`[flywheel] ACCEPTED: change ${path33}`);
|
|
3367
|
+
eventQueue.push("change", path33);
|
|
3368
3368
|
} else {
|
|
3369
|
-
console.error(`[flywheel] FILTERED: change ${
|
|
3369
|
+
console.error(`[flywheel] FILTERED: change ${path33}`);
|
|
3370
3370
|
}
|
|
3371
3371
|
});
|
|
3372
|
-
watcher.on("unlink", (
|
|
3373
|
-
console.error(`[flywheel] RAW EVENT: unlink ${
|
|
3374
|
-
if (shouldWatch(
|
|
3375
|
-
console.error(`[flywheel] ACCEPTED: unlink ${
|
|
3376
|
-
eventQueue.push("unlink",
|
|
3372
|
+
watcher.on("unlink", (path33) => {
|
|
3373
|
+
console.error(`[flywheel] RAW EVENT: unlink ${path33}`);
|
|
3374
|
+
if (shouldWatch(path33, vaultPath2)) {
|
|
3375
|
+
console.error(`[flywheel] ACCEPTED: unlink ${path33}`);
|
|
3376
|
+
eventQueue.push("unlink", path33);
|
|
3377
3377
|
} else {
|
|
3378
|
-
console.error(`[flywheel] FILTERED: unlink ${
|
|
3378
|
+
console.error(`[flywheel] FILTERED: unlink ${path33}`);
|
|
3379
3379
|
}
|
|
3380
3380
|
});
|
|
3381
3381
|
watcher.on("ready", () => {
|
|
@@ -3681,10 +3681,10 @@ function getAllFeedbackBoosts(stateDb2, folder) {
|
|
|
3681
3681
|
for (const row of globalRows) {
|
|
3682
3682
|
let accuracy;
|
|
3683
3683
|
let sampleCount;
|
|
3684
|
-
const
|
|
3685
|
-
if (
|
|
3686
|
-
accuracy =
|
|
3687
|
-
sampleCount =
|
|
3684
|
+
const fs32 = folderStats?.get(row.entity);
|
|
3685
|
+
if (fs32 && fs32.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
|
|
3686
|
+
accuracy = fs32.accuracy;
|
|
3687
|
+
sampleCount = fs32.count;
|
|
3688
3688
|
} else {
|
|
3689
3689
|
accuracy = row.correct_count / row.total;
|
|
3690
3690
|
sampleCount = row.total;
|
|
@@ -7898,14 +7898,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
7898
7898
|
};
|
|
7899
7899
|
function findSimilarEntity2(target, entities) {
|
|
7900
7900
|
const targetLower = target.toLowerCase();
|
|
7901
|
-
for (const [name,
|
|
7901
|
+
for (const [name, path33] of entities) {
|
|
7902
7902
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
7903
|
-
return
|
|
7903
|
+
return path33;
|
|
7904
7904
|
}
|
|
7905
7905
|
}
|
|
7906
|
-
for (const [name,
|
|
7906
|
+
for (const [name, path33] of entities) {
|
|
7907
7907
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
7908
|
-
return
|
|
7908
|
+
return path33;
|
|
7909
7909
|
}
|
|
7910
7910
|
}
|
|
7911
7911
|
return void 0;
|
|
@@ -8574,7 +8574,7 @@ function getSweepResults() {
|
|
|
8574
8574
|
|
|
8575
8575
|
// src/tools/read/health.ts
|
|
8576
8576
|
var STALE_THRESHOLD_SECONDS = 300;
|
|
8577
|
-
function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({}), getStateDb = () => null) {
|
|
8577
|
+
function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({}), getStateDb = () => null, getWatcherStatus2 = () => null) {
|
|
8578
8578
|
const IndexProgressSchema = z3.object({
|
|
8579
8579
|
parsed: z3.coerce.number().describe("Number of files parsed so far"),
|
|
8580
8580
|
total: z3.coerce.number().describe("Total number of files to parse")
|
|
@@ -8647,6 +8647,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8647
8647
|
embeddings_count: z3.coerce.number().describe("Number of notes with semantic embeddings"),
|
|
8648
8648
|
tasks_ready: z3.boolean().describe("Whether the task cache is ready to serve queries"),
|
|
8649
8649
|
tasks_building: z3.boolean().describe("Whether the task cache is currently rebuilding"),
|
|
8650
|
+
watcher_state: z3.enum(["starting", "ready", "rebuilding", "dirty", "error"]).optional().describe("Current file watcher state"),
|
|
8651
|
+
watcher_pending: z3.coerce.number().optional().describe("Number of pending file events in the watcher queue"),
|
|
8650
8652
|
dead_link_count: z3.coerce.number().describe("Total number of broken/dead wikilinks across the vault"),
|
|
8651
8653
|
top_dead_link_targets: z3.array(z3.object({
|
|
8652
8654
|
target: z3.string().describe("The dead link target"),
|
|
@@ -8830,6 +8832,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8830
8832
|
embeddings_count: getEmbeddingsCount(),
|
|
8831
8833
|
tasks_ready: isTaskCacheReady(),
|
|
8832
8834
|
tasks_building: isTaskCacheBuilding(),
|
|
8835
|
+
watcher_state: getWatcherStatus2()?.state,
|
|
8836
|
+
watcher_pending: getWatcherStatus2()?.pendingEvents,
|
|
8833
8837
|
dead_link_count: deadLinkCount,
|
|
8834
8838
|
top_dead_link_targets: topDeadLinkTargets,
|
|
8835
8839
|
sweep: getSweepResults() ?? void 0,
|
|
@@ -8882,8 +8886,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8882
8886
|
daily_counts: z3.record(z3.number())
|
|
8883
8887
|
}).describe("Activity summary for the last 7 days")
|
|
8884
8888
|
};
|
|
8885
|
-
function isPeriodicNote2(
|
|
8886
|
-
const filename =
|
|
8889
|
+
function isPeriodicNote2(path33) {
|
|
8890
|
+
const filename = path33.split("/").pop() || "";
|
|
8887
8891
|
const nameWithoutExt = filename.replace(/\.md$/, "");
|
|
8888
8892
|
const patterns = [
|
|
8889
8893
|
/^\d{4}-\d{2}-\d{2}$/,
|
|
@@ -8898,7 +8902,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8898
8902
|
// YYYY (yearly)
|
|
8899
8903
|
];
|
|
8900
8904
|
const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
|
|
8901
|
-
const folder =
|
|
8905
|
+
const folder = path33.split("/")[0]?.toLowerCase() || "";
|
|
8902
8906
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
8903
8907
|
}
|
|
8904
8908
|
server2.registerTool(
|
|
@@ -10150,18 +10154,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10150
10154
|
include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
|
|
10151
10155
|
}
|
|
10152
10156
|
},
|
|
10153
|
-
async ({ path:
|
|
10157
|
+
async ({ path: path33, include_content }) => {
|
|
10154
10158
|
const index = getIndex();
|
|
10155
10159
|
const vaultPath2 = getVaultPath();
|
|
10156
|
-
const result = await getNoteStructure(index,
|
|
10160
|
+
const result = await getNoteStructure(index, path33, vaultPath2);
|
|
10157
10161
|
if (!result) {
|
|
10158
10162
|
return {
|
|
10159
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
10163
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path33 }, null, 2) }]
|
|
10160
10164
|
};
|
|
10161
10165
|
}
|
|
10162
10166
|
if (include_content) {
|
|
10163
10167
|
for (const section of result.sections) {
|
|
10164
|
-
const sectionResult = await getSectionContent(index,
|
|
10168
|
+
const sectionResult = await getSectionContent(index, path33, section.heading.text, vaultPath2, true);
|
|
10165
10169
|
if (sectionResult) {
|
|
10166
10170
|
section.content = sectionResult.content;
|
|
10167
10171
|
}
|
|
@@ -10183,15 +10187,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10183
10187
|
include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
|
|
10184
10188
|
}
|
|
10185
10189
|
},
|
|
10186
|
-
async ({ path:
|
|
10190
|
+
async ({ path: path33, heading, include_subheadings }) => {
|
|
10187
10191
|
const index = getIndex();
|
|
10188
10192
|
const vaultPath2 = getVaultPath();
|
|
10189
|
-
const result = await getSectionContent(index,
|
|
10193
|
+
const result = await getSectionContent(index, path33, heading, vaultPath2, include_subheadings);
|
|
10190
10194
|
if (!result) {
|
|
10191
10195
|
return {
|
|
10192
10196
|
content: [{ type: "text", text: JSON.stringify({
|
|
10193
10197
|
error: "Section not found",
|
|
10194
|
-
path:
|
|
10198
|
+
path: path33,
|
|
10195
10199
|
heading
|
|
10196
10200
|
}, null, 2) }]
|
|
10197
10201
|
};
|
|
@@ -10245,16 +10249,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10245
10249
|
offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
10246
10250
|
}
|
|
10247
10251
|
},
|
|
10248
|
-
async ({ path:
|
|
10252
|
+
async ({ path: path33, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
|
|
10249
10253
|
const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
|
|
10250
10254
|
const index = getIndex();
|
|
10251
10255
|
const vaultPath2 = getVaultPath();
|
|
10252
10256
|
const config = getConfig();
|
|
10253
|
-
if (
|
|
10254
|
-
const result2 = await getTasksFromNote(index,
|
|
10257
|
+
if (path33) {
|
|
10258
|
+
const result2 = await getTasksFromNote(index, path33, vaultPath2, config.exclude_task_tags || []);
|
|
10255
10259
|
if (!result2) {
|
|
10256
10260
|
return {
|
|
10257
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
10261
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path33 }, null, 2) }]
|
|
10258
10262
|
};
|
|
10259
10263
|
}
|
|
10260
10264
|
let filtered = result2;
|
|
@@ -10264,7 +10268,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10264
10268
|
const paged2 = filtered.slice(offset, offset + limit);
|
|
10265
10269
|
return {
|
|
10266
10270
|
content: [{ type: "text", text: JSON.stringify({
|
|
10267
|
-
path:
|
|
10271
|
+
path: path33,
|
|
10268
10272
|
total_count: filtered.length,
|
|
10269
10273
|
returned_count: paged2.length,
|
|
10270
10274
|
open: result2.filter((t) => t.status === "open").length,
|
|
@@ -16040,8 +16044,160 @@ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
|
16040
16044
|
);
|
|
16041
16045
|
}
|
|
16042
16046
|
|
|
16043
|
-
// src/tools/
|
|
16047
|
+
// src/tools/write/enrich.ts
|
|
16044
16048
|
import { z as z23 } from "zod";
|
|
16049
|
+
import * as fs29 from "fs/promises";
|
|
16050
|
+
import * as path30 from "path";
|
|
16051
|
+
function hasSkipWikilinks(content) {
|
|
16052
|
+
if (!content.startsWith("---")) return false;
|
|
16053
|
+
const endIndex = content.indexOf("\n---", 3);
|
|
16054
|
+
if (endIndex === -1) return false;
|
|
16055
|
+
const frontmatter = content.substring(4, endIndex);
|
|
16056
|
+
return /^skipWikilinks:\s*true\s*$/m.test(frontmatter);
|
|
16057
|
+
}
|
|
16058
|
+
async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
|
|
16059
|
+
const results = [];
|
|
16060
|
+
try {
|
|
16061
|
+
const entries = await fs29.readdir(dirPath, { withFileTypes: true });
|
|
16062
|
+
for (const entry of entries) {
|
|
16063
|
+
if (entry.name.startsWith(".")) continue;
|
|
16064
|
+
const fullPath = path30.join(dirPath, entry.name);
|
|
16065
|
+
if (entry.isDirectory()) {
|
|
16066
|
+
if (excludeFolders.some((f) => entry.name.toLowerCase() === f.toLowerCase())) continue;
|
|
16067
|
+
const sub = await collectMarkdownFiles(fullPath, basePath, excludeFolders);
|
|
16068
|
+
results.push(...sub);
|
|
16069
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
16070
|
+
results.push(path30.relative(basePath, fullPath));
|
|
16071
|
+
}
|
|
16072
|
+
}
|
|
16073
|
+
} catch {
|
|
16074
|
+
}
|
|
16075
|
+
return results;
|
|
16076
|
+
}
|
|
16077
|
+
var EXCLUDE_FOLDERS = [
|
|
16078
|
+
"daily-notes",
|
|
16079
|
+
"daily",
|
|
16080
|
+
"weekly",
|
|
16081
|
+
"weekly-notes",
|
|
16082
|
+
"monthly",
|
|
16083
|
+
"monthly-notes",
|
|
16084
|
+
"quarterly",
|
|
16085
|
+
"yearly-notes",
|
|
16086
|
+
"periodic",
|
|
16087
|
+
"journal",
|
|
16088
|
+
"inbox",
|
|
16089
|
+
"templates",
|
|
16090
|
+
"attachments",
|
|
16091
|
+
"tmp",
|
|
16092
|
+
"clippings",
|
|
16093
|
+
"readwise",
|
|
16094
|
+
"articles",
|
|
16095
|
+
"bookmarks",
|
|
16096
|
+
"web-clips"
|
|
16097
|
+
];
|
|
16098
|
+
function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
16099
|
+
server2.tool(
|
|
16100
|
+
"vault_init",
|
|
16101
|
+
"Initialize vault for Flywheel \u2014 scans legacy notes with zero wikilinks and applies entity links. Safe to re-run (idempotent). Use dry_run (default) to preview.",
|
|
16102
|
+
{
|
|
16103
|
+
dry_run: z23.boolean().default(true).describe("If true (default), preview what would be linked without modifying files"),
|
|
16104
|
+
batch_size: z23.number().default(50).describe("Maximum notes to process per invocation (default: 50)"),
|
|
16105
|
+
offset: z23.number().default(0).describe("Skip this many eligible notes (for pagination across invocations)")
|
|
16106
|
+
},
|
|
16107
|
+
async ({ dry_run, batch_size, offset }) => {
|
|
16108
|
+
const startTime = Date.now();
|
|
16109
|
+
const stateDb2 = getStateDb();
|
|
16110
|
+
const lastRunRow = stateDb2?.getMetadataValue.get("vault_init_last_run_at");
|
|
16111
|
+
const totalEnrichedRow = stateDb2?.getMetadataValue.get("vault_init_total_enriched");
|
|
16112
|
+
const previousTotal = totalEnrichedRow ? parseInt(totalEnrichedRow.value, 10) : 0;
|
|
16113
|
+
checkAndRefreshIfStale();
|
|
16114
|
+
if (!isEntityIndexReady()) {
|
|
16115
|
+
const result = {
|
|
16116
|
+
success: false,
|
|
16117
|
+
mode: dry_run ? "dry_run" : "apply",
|
|
16118
|
+
notes_scanned: 0,
|
|
16119
|
+
notes_with_matches: 0,
|
|
16120
|
+
notes_skipped: 0,
|
|
16121
|
+
total_matches: 0,
|
|
16122
|
+
preview: [],
|
|
16123
|
+
duration_ms: Date.now() - startTime,
|
|
16124
|
+
last_run_at: lastRunRow?.value ?? null,
|
|
16125
|
+
total_enriched: previousTotal
|
|
16126
|
+
};
|
|
16127
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...result, error: "Entity index not ready" }, null, 2) }] };
|
|
16128
|
+
}
|
|
16129
|
+
const allFiles = await collectMarkdownFiles(vaultPath2, vaultPath2, EXCLUDE_FOLDERS);
|
|
16130
|
+
const eligible = [];
|
|
16131
|
+
let notesSkipped = 0;
|
|
16132
|
+
for (const relativePath of allFiles) {
|
|
16133
|
+
const fullPath = path30.join(vaultPath2, relativePath);
|
|
16134
|
+
let content;
|
|
16135
|
+
try {
|
|
16136
|
+
content = await fs29.readFile(fullPath, "utf-8");
|
|
16137
|
+
} catch {
|
|
16138
|
+
continue;
|
|
16139
|
+
}
|
|
16140
|
+
if (hasSkipWikilinks(content)) {
|
|
16141
|
+
notesSkipped++;
|
|
16142
|
+
continue;
|
|
16143
|
+
}
|
|
16144
|
+
const existingLinks = extractLinkedEntities(content);
|
|
16145
|
+
if (existingLinks.size > 0) continue;
|
|
16146
|
+
eligible.push({ relativePath, content });
|
|
16147
|
+
}
|
|
16148
|
+
const paged = eligible.slice(offset, offset + batch_size);
|
|
16149
|
+
const preview = [];
|
|
16150
|
+
let totalMatches = 0;
|
|
16151
|
+
let notesModified = 0;
|
|
16152
|
+
for (const { relativePath, content } of paged) {
|
|
16153
|
+
const result = processWikilinks(content, relativePath);
|
|
16154
|
+
if (result.linksAdded === 0) continue;
|
|
16155
|
+
const entities = result.linkedEntities;
|
|
16156
|
+
totalMatches += result.linksAdded;
|
|
16157
|
+
preview.push({
|
|
16158
|
+
note: relativePath,
|
|
16159
|
+
entities,
|
|
16160
|
+
match_count: result.linksAdded
|
|
16161
|
+
});
|
|
16162
|
+
if (!dry_run) {
|
|
16163
|
+
const fullPath = path30.join(vaultPath2, relativePath);
|
|
16164
|
+
await fs29.writeFile(fullPath, result.content, "utf-8");
|
|
16165
|
+
notesModified++;
|
|
16166
|
+
if (stateDb2) {
|
|
16167
|
+
trackWikilinkApplications(stateDb2, relativePath, entities);
|
|
16168
|
+
const newLinks = extractLinkedEntities(result.content);
|
|
16169
|
+
updateStoredNoteLinks(stateDb2, relativePath, newLinks);
|
|
16170
|
+
}
|
|
16171
|
+
}
|
|
16172
|
+
}
|
|
16173
|
+
if (!dry_run && stateDb2 && notesModified > 0) {
|
|
16174
|
+
const newTotal = previousTotal + notesModified;
|
|
16175
|
+
stateDb2.setMetadataValue.run("vault_init_last_run_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
16176
|
+
stateDb2.setMetadataValue.run("vault_init_total_enriched", String(newTotal));
|
|
16177
|
+
}
|
|
16178
|
+
const currentLastRun = !dry_run && notesModified > 0 ? (/* @__PURE__ */ new Date()).toISOString() : lastRunRow?.value ?? null;
|
|
16179
|
+
const currentTotal = !dry_run ? previousTotal + notesModified : previousTotal;
|
|
16180
|
+
const output = {
|
|
16181
|
+
success: true,
|
|
16182
|
+
mode: dry_run ? "dry_run" : "apply",
|
|
16183
|
+
notes_scanned: allFiles.length,
|
|
16184
|
+
notes_with_matches: preview.length,
|
|
16185
|
+
notes_skipped: notesSkipped,
|
|
16186
|
+
total_matches: totalMatches,
|
|
16187
|
+
...dry_run ? {} : { notes_modified: notesModified },
|
|
16188
|
+
preview: preview.slice(0, 20),
|
|
16189
|
+
// Cap preview to 20 items in output
|
|
16190
|
+
duration_ms: Date.now() - startTime,
|
|
16191
|
+
last_run_at: currentLastRun,
|
|
16192
|
+
total_enriched: currentTotal
|
|
16193
|
+
};
|
|
16194
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
16195
|
+
}
|
|
16196
|
+
);
|
|
16197
|
+
}
|
|
16198
|
+
|
|
16199
|
+
// src/tools/read/metrics.ts
|
|
16200
|
+
import { z as z24 } from "zod";
|
|
16045
16201
|
|
|
16046
16202
|
// src/core/shared/metrics.ts
|
|
16047
16203
|
var ALL_METRICS = [
|
|
@@ -16207,10 +16363,10 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16207
16363
|
title: "Vault Growth",
|
|
16208
16364
|
description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago), "index_activity" (rebuild history). Tracks 11 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio, wikilink_accuracy, wikilink_feedback_volume, wikilink_suppressed_count.',
|
|
16209
16365
|
inputSchema: {
|
|
16210
|
-
mode:
|
|
16211
|
-
metric:
|
|
16212
|
-
days_back:
|
|
16213
|
-
limit:
|
|
16366
|
+
mode: z24.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
|
|
16367
|
+
metric: z24.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
|
|
16368
|
+
days_back: z24.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
|
|
16369
|
+
limit: z24.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
|
|
16214
16370
|
}
|
|
16215
16371
|
},
|
|
16216
16372
|
async ({ mode, metric, days_back, limit: eventLimit }) => {
|
|
@@ -16283,7 +16439,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16283
16439
|
}
|
|
16284
16440
|
|
|
16285
16441
|
// src/tools/read/activity.ts
|
|
16286
|
-
import { z as
|
|
16442
|
+
import { z as z25 } from "zod";
|
|
16287
16443
|
|
|
16288
16444
|
// src/core/shared/toolTracking.ts
|
|
16289
16445
|
function recordToolInvocation(stateDb2, event) {
|
|
@@ -16363,8 +16519,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
|
16363
16519
|
}
|
|
16364
16520
|
}
|
|
16365
16521
|
}
|
|
16366
|
-
return Array.from(noteMap.entries()).map(([
|
|
16367
|
-
path:
|
|
16522
|
+
return Array.from(noteMap.entries()).map(([path33, stats]) => ({
|
|
16523
|
+
path: path33,
|
|
16368
16524
|
access_count: stats.access_count,
|
|
16369
16525
|
last_accessed: stats.last_accessed,
|
|
16370
16526
|
tools_used: Array.from(stats.tools)
|
|
@@ -16443,10 +16599,10 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
16443
16599
|
title: "Vault Activity",
|
|
16444
16600
|
description: 'Track tool usage patterns and session activity. Modes:\n- "session": Current session summary (tools called, notes accessed)\n- "sessions": List of recent sessions\n- "note_access": Notes ranked by query frequency\n- "tool_usage": Tool usage patterns (most-used tools, avg duration)',
|
|
16445
16601
|
inputSchema: {
|
|
16446
|
-
mode:
|
|
16447
|
-
session_id:
|
|
16448
|
-
days_back:
|
|
16449
|
-
limit:
|
|
16602
|
+
mode: z25.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
|
|
16603
|
+
session_id: z25.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
|
|
16604
|
+
days_back: z25.number().optional().describe("Number of days to look back (default: 30)"),
|
|
16605
|
+
limit: z25.number().optional().describe("Maximum results to return (default: 20)")
|
|
16450
16606
|
}
|
|
16451
16607
|
},
|
|
16452
16608
|
async ({ mode, session_id, days_back, limit: resultLimit }) => {
|
|
@@ -16513,11 +16669,11 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
16513
16669
|
}
|
|
16514
16670
|
|
|
16515
16671
|
// src/tools/read/similarity.ts
|
|
16516
|
-
import { z as
|
|
16672
|
+
import { z as z26 } from "zod";
|
|
16517
16673
|
|
|
16518
16674
|
// src/core/read/similarity.ts
|
|
16519
|
-
import * as
|
|
16520
|
-
import * as
|
|
16675
|
+
import * as fs30 from "fs";
|
|
16676
|
+
import * as path31 from "path";
|
|
16521
16677
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
16522
16678
|
"the",
|
|
16523
16679
|
"be",
|
|
@@ -16654,10 +16810,10 @@ function extractKeyTerms(content, maxTerms = 15) {
|
|
|
16654
16810
|
}
|
|
16655
16811
|
function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
16656
16812
|
const limit = options.limit ?? 10;
|
|
16657
|
-
const absPath =
|
|
16813
|
+
const absPath = path31.join(vaultPath2, sourcePath);
|
|
16658
16814
|
let content;
|
|
16659
16815
|
try {
|
|
16660
|
-
content =
|
|
16816
|
+
content = fs30.readFileSync(absPath, "utf-8");
|
|
16661
16817
|
} catch {
|
|
16662
16818
|
return [];
|
|
16663
16819
|
}
|
|
@@ -16777,12 +16933,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16777
16933
|
title: "Find Similar Notes",
|
|
16778
16934
|
description: "Find notes similar to a given note using FTS5 keyword matching. When embeddings have been built (via init_semantic), automatically uses hybrid ranking (BM25 + embedding similarity via Reciprocal Rank Fusion). Use exclude_linked to filter out notes already connected via wikilinks.",
|
|
16779
16935
|
inputSchema: {
|
|
16780
|
-
path:
|
|
16781
|
-
limit:
|
|
16782
|
-
exclude_linked:
|
|
16936
|
+
path: z26.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
|
|
16937
|
+
limit: z26.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
|
|
16938
|
+
exclude_linked: z26.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
|
|
16783
16939
|
}
|
|
16784
16940
|
},
|
|
16785
|
-
async ({ path:
|
|
16941
|
+
async ({ path: path33, limit, exclude_linked }) => {
|
|
16786
16942
|
const index = getIndex();
|
|
16787
16943
|
const vaultPath2 = getVaultPath();
|
|
16788
16944
|
const stateDb2 = getStateDb();
|
|
@@ -16791,10 +16947,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16791
16947
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
16792
16948
|
};
|
|
16793
16949
|
}
|
|
16794
|
-
if (!index.notes.has(
|
|
16950
|
+
if (!index.notes.has(path33)) {
|
|
16795
16951
|
return {
|
|
16796
16952
|
content: [{ type: "text", text: JSON.stringify({
|
|
16797
|
-
error: `Note not found: ${
|
|
16953
|
+
error: `Note not found: ${path33}`,
|
|
16798
16954
|
hint: "Use the full relative path including .md extension"
|
|
16799
16955
|
}, null, 2) }]
|
|
16800
16956
|
};
|
|
@@ -16805,12 +16961,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16805
16961
|
};
|
|
16806
16962
|
const useHybrid = hasEmbeddingsIndex();
|
|
16807
16963
|
const method = useHybrid ? "hybrid" : "bm25";
|
|
16808
|
-
const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index,
|
|
16964
|
+
const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path33, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path33, opts);
|
|
16809
16965
|
return {
|
|
16810
16966
|
content: [{
|
|
16811
16967
|
type: "text",
|
|
16812
16968
|
text: JSON.stringify({
|
|
16813
|
-
source:
|
|
16969
|
+
source: path33,
|
|
16814
16970
|
method,
|
|
16815
16971
|
exclude_linked: exclude_linked ?? true,
|
|
16816
16972
|
count: results.length,
|
|
@@ -16823,7 +16979,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16823
16979
|
}
|
|
16824
16980
|
|
|
16825
16981
|
// src/tools/read/semantic.ts
|
|
16826
|
-
import { z as
|
|
16982
|
+
import { z as z27 } from "zod";
|
|
16827
16983
|
import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
|
|
16828
16984
|
function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
16829
16985
|
server2.registerTool(
|
|
@@ -16832,7 +16988,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
16832
16988
|
title: "Initialize Semantic Search",
|
|
16833
16989
|
description: "Download the embedding model and build semantic search index for this vault. After running, search and find_similar automatically use hybrid ranking (BM25 + semantic). Run once per vault \u2014 subsequent calls skip already-embedded notes unless force=true.",
|
|
16834
16990
|
inputSchema: {
|
|
16835
|
-
force:
|
|
16991
|
+
force: z27.boolean().optional().describe(
|
|
16836
16992
|
"Rebuild all embeddings even if they already exist (default: false)"
|
|
16837
16993
|
)
|
|
16838
16994
|
}
|
|
@@ -16912,7 +17068,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
16912
17068
|
|
|
16913
17069
|
// src/tools/read/merges.ts
|
|
16914
17070
|
init_levenshtein();
|
|
16915
|
-
import { z as
|
|
17071
|
+
import { z as z28 } from "zod";
|
|
16916
17072
|
import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
|
|
16917
17073
|
function normalizeName(name) {
|
|
16918
17074
|
return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
|
|
@@ -16922,7 +17078,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
16922
17078
|
"suggest_entity_merges",
|
|
16923
17079
|
"Find potential duplicate entities that could be merged based on name similarity",
|
|
16924
17080
|
{
|
|
16925
|
-
limit:
|
|
17081
|
+
limit: z28.number().optional().default(50).describe("Maximum number of suggestions to return")
|
|
16926
17082
|
},
|
|
16927
17083
|
async ({ limit }) => {
|
|
16928
17084
|
const stateDb2 = getStateDb();
|
|
@@ -17024,11 +17180,11 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
17024
17180
|
"dismiss_merge_suggestion",
|
|
17025
17181
|
"Permanently dismiss a merge suggestion so it never reappears",
|
|
17026
17182
|
{
|
|
17027
|
-
source_path:
|
|
17028
|
-
target_path:
|
|
17029
|
-
source_name:
|
|
17030
|
-
target_name:
|
|
17031
|
-
reason:
|
|
17183
|
+
source_path: z28.string().describe("Path of the source entity"),
|
|
17184
|
+
target_path: z28.string().describe("Path of the target entity"),
|
|
17185
|
+
source_name: z28.string().describe("Name of the source entity"),
|
|
17186
|
+
target_name: z28.string().describe("Name of the target entity"),
|
|
17187
|
+
reason: z28.string().describe("Original suggestion reason")
|
|
17032
17188
|
},
|
|
17033
17189
|
async ({ source_path, target_path, source_name, target_name, reason }) => {
|
|
17034
17190
|
const stateDb2 = getStateDb();
|
|
@@ -17047,7 +17203,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
17047
17203
|
}
|
|
17048
17204
|
|
|
17049
17205
|
// src/index.ts
|
|
17050
|
-
import * as
|
|
17206
|
+
import * as fs31 from "node:fs/promises";
|
|
17051
17207
|
import { createHash as createHash2 } from "node:crypto";
|
|
17052
17208
|
|
|
17053
17209
|
// src/resources/vault.ts
|
|
@@ -17156,7 +17312,7 @@ function registerVaultResources(server2, getIndex) {
|
|
|
17156
17312
|
// src/index.ts
|
|
17157
17313
|
var __filename = fileURLToPath(import.meta.url);
|
|
17158
17314
|
var __dirname = dirname4(__filename);
|
|
17159
|
-
var pkg = JSON.parse(readFileSync4(
|
|
17315
|
+
var pkg = JSON.parse(readFileSync4(join17(__dirname, "../package.json"), "utf-8"));
|
|
17160
17316
|
var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
17161
17317
|
var resolvedVaultPath;
|
|
17162
17318
|
try {
|
|
@@ -17167,6 +17323,10 @@ try {
|
|
|
17167
17323
|
var vaultIndex;
|
|
17168
17324
|
var flywheelConfig = {};
|
|
17169
17325
|
var stateDb = null;
|
|
17326
|
+
var watcherStatus = null;
|
|
17327
|
+
function getWatcherStatus() {
|
|
17328
|
+
return watcherStatus;
|
|
17329
|
+
}
|
|
17170
17330
|
var PRESETS = {
|
|
17171
17331
|
// Presets
|
|
17172
17332
|
minimal: ["search", "structure", "append", "frontmatter", "notes"],
|
|
@@ -17391,7 +17551,7 @@ if (_originalRegisterTool) {
|
|
|
17391
17551
|
}
|
|
17392
17552
|
var categoryList = Array.from(enabledCategories).sort().join(", ");
|
|
17393
17553
|
serverLog("server", `Tool categories: ${categoryList}`);
|
|
17394
|
-
registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
|
|
17554
|
+
registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb, getWatcherStatus);
|
|
17395
17555
|
registerSystemTools(
|
|
17396
17556
|
server,
|
|
17397
17557
|
() => vaultIndex,
|
|
@@ -17423,6 +17583,7 @@ registerSystemTools2(server, vaultPath);
|
|
|
17423
17583
|
registerPolicyTools(server, vaultPath);
|
|
17424
17584
|
registerTagTools(server, () => vaultIndex, () => vaultPath);
|
|
17425
17585
|
registerWikilinkFeedbackTools(server, () => stateDb);
|
|
17586
|
+
registerInitTools(server, vaultPath, () => stateDb);
|
|
17426
17587
|
registerConfigTools(
|
|
17427
17588
|
server,
|
|
17428
17589
|
() => flywheelConfig,
|
|
@@ -17460,6 +17621,10 @@ async function main() {
|
|
|
17460
17621
|
setWriteStateDb(stateDb);
|
|
17461
17622
|
setRecencyStateDb(stateDb);
|
|
17462
17623
|
setEdgeWeightStateDb(stateDb);
|
|
17624
|
+
const vaultInitRow = stateDb.getMetadataValue.get("vault_init_last_run_at");
|
|
17625
|
+
if (!vaultInitRow) {
|
|
17626
|
+
serverLog("server", "Vault not initialized \u2014 call vault_init to enrich legacy notes");
|
|
17627
|
+
}
|
|
17463
17628
|
} catch (err) {
|
|
17464
17629
|
const msg = err instanceof Error ? err.message : String(err);
|
|
17465
17630
|
serverLog("statedb", `StateDb initialization failed: ${msg}`, "error");
|
|
@@ -17577,22 +17742,22 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
|
17577
17742
|
async function scanDir(dir) {
|
|
17578
17743
|
let entries;
|
|
17579
17744
|
try {
|
|
17580
|
-
entries = await
|
|
17745
|
+
entries = await fs31.readdir(dir, { withFileTypes: true });
|
|
17581
17746
|
} catch {
|
|
17582
17747
|
return;
|
|
17583
17748
|
}
|
|
17584
17749
|
for (const entry of entries) {
|
|
17585
|
-
const fullPath =
|
|
17750
|
+
const fullPath = path32.join(dir, entry.name);
|
|
17586
17751
|
if (entry.isDirectory()) {
|
|
17587
17752
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
17588
17753
|
await scanDir(fullPath);
|
|
17589
17754
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
17590
17755
|
try {
|
|
17591
|
-
const stat4 = await
|
|
17756
|
+
const stat4 = await fs31.stat(fullPath);
|
|
17592
17757
|
if (stat4.mtimeMs > sinceMs) {
|
|
17593
17758
|
events.push({
|
|
17594
17759
|
type: "upsert",
|
|
17595
|
-
path:
|
|
17760
|
+
path: path32.relative(vaultPath2, fullPath),
|
|
17596
17761
|
originalEvents: []
|
|
17597
17762
|
});
|
|
17598
17763
|
}
|
|
@@ -17717,8 +17882,8 @@ async function runPostIndexWork(index) {
|
|
|
17717
17882
|
}
|
|
17718
17883
|
} catch {
|
|
17719
17884
|
try {
|
|
17720
|
-
const dir =
|
|
17721
|
-
const base =
|
|
17885
|
+
const dir = path32.dirname(rawPath);
|
|
17886
|
+
const base = path32.basename(rawPath);
|
|
17722
17887
|
const resolvedDir = realpathSync(dir).replace(/\\/g, "/");
|
|
17723
17888
|
for (const prefix of vaultPrefixes) {
|
|
17724
17889
|
if (resolvedDir.startsWith(prefix + "/") || resolvedDir === prefix) {
|
|
@@ -17747,7 +17912,7 @@ async function runPostIndexWork(index) {
|
|
|
17747
17912
|
continue;
|
|
17748
17913
|
}
|
|
17749
17914
|
try {
|
|
17750
|
-
const content = await
|
|
17915
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
17751
17916
|
const hash = createHash2("md5").update(content).digest("hex");
|
|
17752
17917
|
if (lastContentHashes.get(event.path) === hash) {
|
|
17753
17918
|
serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
|
|
@@ -17818,7 +17983,7 @@ async function runPostIndexWork(index) {
|
|
|
17818
17983
|
...batch,
|
|
17819
17984
|
events: filteredEvents.map((e) => ({
|
|
17820
17985
|
...e,
|
|
17821
|
-
path:
|
|
17986
|
+
path: path32.join(vaultPath, e.path)
|
|
17822
17987
|
}))
|
|
17823
17988
|
};
|
|
17824
17989
|
const batchResult = await processBatch(vaultIndex, vaultPath, absoluteBatch);
|
|
@@ -17952,7 +18117,7 @@ async function runPostIndexWork(index) {
|
|
|
17952
18117
|
removeEmbedding(event.path);
|
|
17953
18118
|
embRemoved++;
|
|
17954
18119
|
} else if (event.path.endsWith(".md")) {
|
|
17955
|
-
const absPath =
|
|
18120
|
+
const absPath = path32.join(vaultPath, event.path);
|
|
17956
18121
|
await updateEmbedding(event.path, absPath);
|
|
17957
18122
|
embUpdated++;
|
|
17958
18123
|
}
|
|
@@ -18159,7 +18324,7 @@ async function runPostIndexWork(index) {
|
|
|
18159
18324
|
for (const event of filteredEvents) {
|
|
18160
18325
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
18161
18326
|
try {
|
|
18162
|
-
const content = await
|
|
18327
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
18163
18328
|
const zones = getProtectedZones2(content);
|
|
18164
18329
|
const linked = new Set(
|
|
18165
18330
|
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).map((n) => n.toLowerCase())
|
|
@@ -18195,7 +18360,7 @@ async function runPostIndexWork(index) {
|
|
|
18195
18360
|
for (const event of filteredEvents) {
|
|
18196
18361
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
18197
18362
|
try {
|
|
18198
|
-
const content = await
|
|
18363
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
18199
18364
|
const removed = processImplicitFeedback(stateDb, event.path, content);
|
|
18200
18365
|
for (const entity of removed) feedbackResults.push({ entity, file: event.path });
|
|
18201
18366
|
} catch {
|
|
@@ -18314,6 +18479,7 @@ async function runPostIndexWork(index) {
|
|
|
18314
18479
|
config,
|
|
18315
18480
|
onBatch: handleBatch,
|
|
18316
18481
|
onStateChange: (status) => {
|
|
18482
|
+
watcherStatus = status;
|
|
18317
18483
|
if (status.state === "dirty") {
|
|
18318
18484
|
serverLog("watcher", "Index may be stale", "warn");
|
|
18319
18485
|
}
|
|
@@ -18369,3 +18535,6 @@ if (process.argv.includes("--init-semantic")) {
|
|
|
18369
18535
|
process.on("beforeExit", async () => {
|
|
18370
18536
|
await flushLogs();
|
|
18371
18537
|
});
|
|
18538
|
+
export {
|
|
18539
|
+
getWatcherStatus
|
|
18540
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.46",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
55
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
55
|
+
"@velvetmonkey/vault-core": "^2.0.46",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|