@velvetmonkey/flywheel-memory 2.0.43 → 2.0.45
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 +366 -129
- 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", () => {
|
|
@@ -3627,9 +3627,12 @@ function getSuppressedCount(stateDb2) {
|
|
|
3627
3627
|
return row.count;
|
|
3628
3628
|
}
|
|
3629
3629
|
function getSuppressedEntities(stateDb2) {
|
|
3630
|
-
return stateDb2.db.prepare(
|
|
3631
|
-
|
|
3632
|
-
|
|
3630
|
+
return stateDb2.db.prepare(`
|
|
3631
|
+
SELECT s.entity, s.false_positive_rate,
|
|
3632
|
+
COALESCE((SELECT COUNT(*) FROM wikilink_feedback WHERE entity = s.entity), 0) as total
|
|
3633
|
+
FROM wikilink_suppressions s
|
|
3634
|
+
ORDER BY s.false_positive_rate DESC
|
|
3635
|
+
`).all();
|
|
3633
3636
|
}
|
|
3634
3637
|
function computeBoostFromAccuracy(accuracy, sampleCount) {
|
|
3635
3638
|
if (sampleCount < FEEDBACK_BOOST_MIN_SAMPLES) return 0;
|
|
@@ -3678,10 +3681,10 @@ function getAllFeedbackBoosts(stateDb2, folder) {
|
|
|
3678
3681
|
for (const row of globalRows) {
|
|
3679
3682
|
let accuracy;
|
|
3680
3683
|
let sampleCount;
|
|
3681
|
-
const
|
|
3682
|
-
if (
|
|
3683
|
-
accuracy =
|
|
3684
|
-
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;
|
|
3685
3688
|
} else {
|
|
3686
3689
|
accuracy = row.correct_count / row.total;
|
|
3687
3690
|
sampleCount = row.total;
|
|
@@ -3784,6 +3787,9 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
|
|
|
3784
3787
|
}
|
|
3785
3788
|
});
|
|
3786
3789
|
transaction();
|
|
3790
|
+
if (removed.length > 0) {
|
|
3791
|
+
updateSuppressionList(stateDb2);
|
|
3792
|
+
}
|
|
3787
3793
|
return removed;
|
|
3788
3794
|
}
|
|
3789
3795
|
var TIER_LABELS = [
|
|
@@ -5407,7 +5413,7 @@ function recomputeEdgeWeights(stateDb2) {
|
|
|
5407
5413
|
"SELECT note_path, target FROM note_links"
|
|
5408
5414
|
).all();
|
|
5409
5415
|
if (edges.length === 0) {
|
|
5410
|
-
return { edges_updated: 0, duration_ms: Date.now() - start, total_weighted: 0, avg_weight: 0, strong_count: 0 };
|
|
5416
|
+
return { edges_updated: 0, duration_ms: Date.now() - start, total_weighted: 0, avg_weight: 0, strong_count: 0, top_changes: [] };
|
|
5411
5417
|
}
|
|
5412
5418
|
const survivalMap = /* @__PURE__ */ new Map();
|
|
5413
5419
|
const historyRows = stateDb2.db.prepare(
|
|
@@ -5471,10 +5477,18 @@ function recomputeEdgeWeights(stateDb2) {
|
|
|
5471
5477
|
}
|
|
5472
5478
|
}
|
|
5473
5479
|
}
|
|
5480
|
+
const oldWeights = /* @__PURE__ */ new Map();
|
|
5481
|
+
const oldRows = stateDb2.db.prepare(
|
|
5482
|
+
"SELECT note_path, target, weight FROM note_links"
|
|
5483
|
+
).all();
|
|
5484
|
+
for (const row of oldRows) {
|
|
5485
|
+
oldWeights.set(`${row.note_path}\0${row.target}`, row.weight);
|
|
5486
|
+
}
|
|
5474
5487
|
const now = Date.now();
|
|
5475
5488
|
const update = stateDb2.db.prepare(
|
|
5476
5489
|
"UPDATE note_links SET weight = ?, weight_updated_at = ? WHERE note_path = ? AND target = ?"
|
|
5477
5490
|
);
|
|
5491
|
+
const changes = [];
|
|
5478
5492
|
const tx = stateDb2.db.transaction(() => {
|
|
5479
5493
|
for (const edge of edges) {
|
|
5480
5494
|
const edgeKey = `${edge.note_path}\0${edge.target}`;
|
|
@@ -5482,10 +5496,27 @@ function recomputeEdgeWeights(stateDb2) {
|
|
|
5482
5496
|
const coSessions = coSessionCount.get(edgeKey) ?? 0;
|
|
5483
5497
|
const sourceAccess = sourceActivityCount.get(edge.note_path) ?? 0;
|
|
5484
5498
|
const weight = 1 + editsSurvived * 0.5 + Math.min(coSessions * 0.5, 3) + Math.min(sourceAccess * 0.2, 2);
|
|
5485
|
-
|
|
5499
|
+
const roundedWeight = Math.round(weight * 1e3) / 1e3;
|
|
5500
|
+
const oldWeight = oldWeights.get(edgeKey) ?? 1;
|
|
5501
|
+
const delta = roundedWeight - oldWeight;
|
|
5502
|
+
if (Math.abs(delta) >= 1e-3) {
|
|
5503
|
+
changes.push({
|
|
5504
|
+
note_path: edge.note_path,
|
|
5505
|
+
target: edge.target,
|
|
5506
|
+
old_weight: oldWeight,
|
|
5507
|
+
new_weight: roundedWeight,
|
|
5508
|
+
delta: Math.round(delta * 1e3) / 1e3,
|
|
5509
|
+
edits_survived: editsSurvived,
|
|
5510
|
+
co_sessions: coSessions,
|
|
5511
|
+
source_access: sourceAccess
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5514
|
+
update.run(roundedWeight, now, edge.note_path, edge.target);
|
|
5486
5515
|
}
|
|
5487
5516
|
});
|
|
5488
5517
|
tx();
|
|
5518
|
+
changes.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
5519
|
+
const top_changes = changes.slice(0, 10);
|
|
5489
5520
|
const stats = stateDb2.db.prepare(`
|
|
5490
5521
|
SELECT
|
|
5491
5522
|
COUNT(*) as total_weighted,
|
|
@@ -5499,7 +5530,8 @@ function recomputeEdgeWeights(stateDb2) {
|
|
|
5499
5530
|
duration_ms: Date.now() - start,
|
|
5500
5531
|
total_weighted: stats?.total_weighted ?? 0,
|
|
5501
5532
|
avg_weight: Math.round((stats?.avg_weight ?? 0) * 100) / 100,
|
|
5502
|
-
strong_count: stats?.strong_count ?? 0
|
|
5533
|
+
strong_count: stats?.strong_count ?? 0,
|
|
5534
|
+
top_changes
|
|
5503
5535
|
};
|
|
5504
5536
|
}
|
|
5505
5537
|
function getEntityEdgeWeightMap(stateDb2) {
|
|
@@ -6202,6 +6234,12 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6202
6234
|
if (linkedEntities.has(entityName.toLowerCase())) {
|
|
6203
6235
|
continue;
|
|
6204
6236
|
}
|
|
6237
|
+
if (moduleStateDb5 && !disabled.has("feedback")) {
|
|
6238
|
+
const noteFolder2 = notePath ? notePath.split("/").slice(0, -1).join("/") : void 0;
|
|
6239
|
+
if (isSuppressed(moduleStateDb5, entityName, noteFolder2)) {
|
|
6240
|
+
continue;
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6205
6243
|
const contentScore = disabled.has("exact_match") && disabled.has("stem_match") ? 0 : scoreEntity(entity, contentTokens, contentStems, config);
|
|
6206
6244
|
let score = contentScore;
|
|
6207
6245
|
if (contentScore > 0) {
|
|
@@ -6251,6 +6289,10 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6251
6289
|
if (!disabled.has("length_filter") && entityName.length > MAX_ENTITY_LENGTH) continue;
|
|
6252
6290
|
if (!disabled.has("article_filter") && isLikelyArticleTitle(entityName)) continue;
|
|
6253
6291
|
if (linkedEntities.has(entityName.toLowerCase())) continue;
|
|
6292
|
+
if (moduleStateDb5 && !disabled.has("feedback")) {
|
|
6293
|
+
const noteFolder2 = notePath ? notePath.split("/").slice(0, -1).join("/") : void 0;
|
|
6294
|
+
if (isSuppressed(moduleStateDb5, entityName, noteFolder2)) continue;
|
|
6295
|
+
}
|
|
6254
6296
|
const boost = getCooccurrenceBoost(entityName, directlyMatchedEntities, cooccurrenceIndex, recencyIndex);
|
|
6255
6297
|
if (boost > 0) {
|
|
6256
6298
|
const existing = scoredEntities.find((e) => e.name === entityName);
|
|
@@ -6315,6 +6357,10 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6315
6357
|
existing.score += boost;
|
|
6316
6358
|
existing.breakdown.semanticBoost = boost;
|
|
6317
6359
|
} else if (!linkedEntities.has(match.entityName.toLowerCase())) {
|
|
6360
|
+
if (moduleStateDb5 && !disabled.has("feedback")) {
|
|
6361
|
+
const noteFolder2 = notePath ? notePath.split("/").slice(0, -1).join("/") : void 0;
|
|
6362
|
+
if (isSuppressed(moduleStateDb5, match.entityName, noteFolder2)) continue;
|
|
6363
|
+
}
|
|
6318
6364
|
const entityWithType = entitiesWithTypes.find(
|
|
6319
6365
|
(et) => et.entity.name === match.entityName
|
|
6320
6366
|
);
|
|
@@ -6419,7 +6465,9 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6419
6465
|
if (topSuggestions.length === 0) {
|
|
6420
6466
|
return emptyResult;
|
|
6421
6467
|
}
|
|
6422
|
-
const
|
|
6468
|
+
const MIN_SUFFIX_SCORE = 12;
|
|
6469
|
+
const suffixEntries = topEntries.filter((e) => e.score >= MIN_SUFFIX_SCORE);
|
|
6470
|
+
const suffix = suffixEntries.length > 0 ? "\u2192 " + suffixEntries.map((e) => `[[${e.name}]]`).join(", ") : "";
|
|
6423
6471
|
const result = {
|
|
6424
6472
|
suggestions: topSuggestions,
|
|
6425
6473
|
suffix
|
|
@@ -7850,14 +7898,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
7850
7898
|
};
|
|
7851
7899
|
function findSimilarEntity2(target, entities) {
|
|
7852
7900
|
const targetLower = target.toLowerCase();
|
|
7853
|
-
for (const [name,
|
|
7901
|
+
for (const [name, path33] of entities) {
|
|
7854
7902
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
7855
|
-
return
|
|
7903
|
+
return path33;
|
|
7856
7904
|
}
|
|
7857
7905
|
}
|
|
7858
|
-
for (const [name,
|
|
7906
|
+
for (const [name, path33] of entities) {
|
|
7859
7907
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
7860
|
-
return
|
|
7908
|
+
return path33;
|
|
7861
7909
|
}
|
|
7862
7910
|
}
|
|
7863
7911
|
return void 0;
|
|
@@ -8834,8 +8882,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8834
8882
|
daily_counts: z3.record(z3.number())
|
|
8835
8883
|
}).describe("Activity summary for the last 7 days")
|
|
8836
8884
|
};
|
|
8837
|
-
function isPeriodicNote2(
|
|
8838
|
-
const filename =
|
|
8885
|
+
function isPeriodicNote2(path33) {
|
|
8886
|
+
const filename = path33.split("/").pop() || "";
|
|
8839
8887
|
const nameWithoutExt = filename.replace(/\.md$/, "");
|
|
8840
8888
|
const patterns = [
|
|
8841
8889
|
/^\d{4}-\d{2}-\d{2}$/,
|
|
@@ -8850,7 +8898,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8850
8898
|
// YYYY (yearly)
|
|
8851
8899
|
];
|
|
8852
8900
|
const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
|
|
8853
|
-
const folder =
|
|
8901
|
+
const folder = path33.split("/")[0]?.toLowerCase() || "";
|
|
8854
8902
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
8855
8903
|
}
|
|
8856
8904
|
server2.registerTool(
|
|
@@ -10102,18 +10150,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10102
10150
|
include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
|
|
10103
10151
|
}
|
|
10104
10152
|
},
|
|
10105
|
-
async ({ path:
|
|
10153
|
+
async ({ path: path33, include_content }) => {
|
|
10106
10154
|
const index = getIndex();
|
|
10107
10155
|
const vaultPath2 = getVaultPath();
|
|
10108
|
-
const result = await getNoteStructure(index,
|
|
10156
|
+
const result = await getNoteStructure(index, path33, vaultPath2);
|
|
10109
10157
|
if (!result) {
|
|
10110
10158
|
return {
|
|
10111
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
10159
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path33 }, null, 2) }]
|
|
10112
10160
|
};
|
|
10113
10161
|
}
|
|
10114
10162
|
if (include_content) {
|
|
10115
10163
|
for (const section of result.sections) {
|
|
10116
|
-
const sectionResult = await getSectionContent(index,
|
|
10164
|
+
const sectionResult = await getSectionContent(index, path33, section.heading.text, vaultPath2, true);
|
|
10117
10165
|
if (sectionResult) {
|
|
10118
10166
|
section.content = sectionResult.content;
|
|
10119
10167
|
}
|
|
@@ -10135,15 +10183,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10135
10183
|
include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
|
|
10136
10184
|
}
|
|
10137
10185
|
},
|
|
10138
|
-
async ({ path:
|
|
10186
|
+
async ({ path: path33, heading, include_subheadings }) => {
|
|
10139
10187
|
const index = getIndex();
|
|
10140
10188
|
const vaultPath2 = getVaultPath();
|
|
10141
|
-
const result = await getSectionContent(index,
|
|
10189
|
+
const result = await getSectionContent(index, path33, heading, vaultPath2, include_subheadings);
|
|
10142
10190
|
if (!result) {
|
|
10143
10191
|
return {
|
|
10144
10192
|
content: [{ type: "text", text: JSON.stringify({
|
|
10145
10193
|
error: "Section not found",
|
|
10146
|
-
path:
|
|
10194
|
+
path: path33,
|
|
10147
10195
|
heading
|
|
10148
10196
|
}, null, 2) }]
|
|
10149
10197
|
};
|
|
@@ -10197,16 +10245,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10197
10245
|
offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
10198
10246
|
}
|
|
10199
10247
|
},
|
|
10200
|
-
async ({ path:
|
|
10248
|
+
async ({ path: path33, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
|
|
10201
10249
|
const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
|
|
10202
10250
|
const index = getIndex();
|
|
10203
10251
|
const vaultPath2 = getVaultPath();
|
|
10204
10252
|
const config = getConfig();
|
|
10205
|
-
if (
|
|
10206
|
-
const result2 = await getTasksFromNote(index,
|
|
10253
|
+
if (path33) {
|
|
10254
|
+
const result2 = await getTasksFromNote(index, path33, vaultPath2, config.exclude_task_tags || []);
|
|
10207
10255
|
if (!result2) {
|
|
10208
10256
|
return {
|
|
10209
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
10257
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path33 }, null, 2) }]
|
|
10210
10258
|
};
|
|
10211
10259
|
}
|
|
10212
10260
|
let filtered = result2;
|
|
@@ -10216,7 +10264,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10216
10264
|
const paged2 = filtered.slice(offset, offset + limit);
|
|
10217
10265
|
return {
|
|
10218
10266
|
content: [{ type: "text", text: JSON.stringify({
|
|
10219
|
-
path:
|
|
10267
|
+
path: path33,
|
|
10220
10268
|
total_count: filtered.length,
|
|
10221
10269
|
returned_count: paged2.length,
|
|
10222
10270
|
open: result2.filter((t) => t.status === "open").length,
|
|
@@ -12892,7 +12940,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
12892
12940
|
wikilinkInfo: wikilinkInfo || "none"
|
|
12893
12941
|
};
|
|
12894
12942
|
let suggestInfo;
|
|
12895
|
-
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
12943
|
+
if (suggestOutgoingLinks && !skipWikilinks && processedContent.length >= 100) {
|
|
12896
12944
|
const result = await suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
|
|
12897
12945
|
if (result.suffix) {
|
|
12898
12946
|
processedContent = processedContent + " " + result.suffix;
|
|
@@ -13011,7 +13059,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
13011
13059
|
}
|
|
13012
13060
|
let workingReplacement = validationResult.content;
|
|
13013
13061
|
let { content: processedReplacement } = maybeApplyWikilinks(workingReplacement, skipWikilinks, notePath);
|
|
13014
|
-
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
13062
|
+
if (suggestOutgoingLinks && !skipWikilinks && processedReplacement.length >= 100) {
|
|
13015
13063
|
const result = await suggestRelatedLinks(processedReplacement, { maxSuggestions, notePath });
|
|
13016
13064
|
if (result.suffix) {
|
|
13017
13065
|
processedReplacement = processedReplacement + " " + result.suffix;
|
|
@@ -13392,7 +13440,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
13392
13440
|
}
|
|
13393
13441
|
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(effectiveContent, skipWikilinks, notePath);
|
|
13394
13442
|
let suggestInfo;
|
|
13395
|
-
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
13443
|
+
if (suggestOutgoingLinks && !skipWikilinks && processedContent.length >= 100) {
|
|
13396
13444
|
const result = await suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
|
|
13397
13445
|
if (result.suffix) {
|
|
13398
13446
|
processedContent = processedContent + " " + result.suffix;
|
|
@@ -15814,10 +15862,11 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
15814
15862
|
days_back: z21.number().optional().describe("Days to look back (default: 30)"),
|
|
15815
15863
|
granularity: z21.enum(["day", "week"]).optional().describe("Time bucket granularity for layer_timeseries (default: day)"),
|
|
15816
15864
|
timestamp_before: z21.number().optional().describe("Earlier timestamp for snapshot_diff"),
|
|
15817
|
-
timestamp_after: z21.number().optional().describe("Later timestamp for snapshot_diff")
|
|
15865
|
+
timestamp_after: z21.number().optional().describe("Later timestamp for snapshot_diff"),
|
|
15866
|
+
skip_status_update: z21.boolean().optional().describe("Skip marking application as removed (caller will trigger implicit detection via file edit)")
|
|
15818
15867
|
}
|
|
15819
15868
|
},
|
|
15820
|
-
async ({ mode, entity, note_path, context, correct, limit, days_back, granularity, timestamp_before, timestamp_after }) => {
|
|
15869
|
+
async ({ mode, entity, note_path, context, correct, limit, days_back, granularity, timestamp_before, timestamp_after, skip_status_update }) => {
|
|
15821
15870
|
const stateDb2 = getStateDb();
|
|
15822
15871
|
if (!stateDb2) {
|
|
15823
15872
|
return {
|
|
@@ -15845,6 +15894,11 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
15845
15894
|
isError: true
|
|
15846
15895
|
};
|
|
15847
15896
|
}
|
|
15897
|
+
if (!correct && note_path && !skip_status_update) {
|
|
15898
|
+
stateDb2.db.prepare(
|
|
15899
|
+
`UPDATE wikilink_applications SET status = 'removed' WHERE entity = ? AND note_path = ? COLLATE NOCASE`
|
|
15900
|
+
).run(entity, note_path);
|
|
15901
|
+
}
|
|
15848
15902
|
const suppressionUpdated = updateSuppressionList(stateDb2) > 0;
|
|
15849
15903
|
result = {
|
|
15850
15904
|
mode: "report",
|
|
@@ -15986,8 +16040,160 @@ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
|
15986
16040
|
);
|
|
15987
16041
|
}
|
|
15988
16042
|
|
|
15989
|
-
// src/tools/
|
|
16043
|
+
// src/tools/write/enrich.ts
|
|
15990
16044
|
import { z as z23 } from "zod";
|
|
16045
|
+
import * as fs29 from "fs/promises";
|
|
16046
|
+
import * as path30 from "path";
|
|
16047
|
+
function hasSkipWikilinks(content) {
|
|
16048
|
+
if (!content.startsWith("---")) return false;
|
|
16049
|
+
const endIndex = content.indexOf("\n---", 3);
|
|
16050
|
+
if (endIndex === -1) return false;
|
|
16051
|
+
const frontmatter = content.substring(4, endIndex);
|
|
16052
|
+
return /^skipWikilinks:\s*true\s*$/m.test(frontmatter);
|
|
16053
|
+
}
|
|
16054
|
+
async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
|
|
16055
|
+
const results = [];
|
|
16056
|
+
try {
|
|
16057
|
+
const entries = await fs29.readdir(dirPath, { withFileTypes: true });
|
|
16058
|
+
for (const entry of entries) {
|
|
16059
|
+
if (entry.name.startsWith(".")) continue;
|
|
16060
|
+
const fullPath = path30.join(dirPath, entry.name);
|
|
16061
|
+
if (entry.isDirectory()) {
|
|
16062
|
+
if (excludeFolders.some((f) => entry.name.toLowerCase() === f.toLowerCase())) continue;
|
|
16063
|
+
const sub = await collectMarkdownFiles(fullPath, basePath, excludeFolders);
|
|
16064
|
+
results.push(...sub);
|
|
16065
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
16066
|
+
results.push(path30.relative(basePath, fullPath));
|
|
16067
|
+
}
|
|
16068
|
+
}
|
|
16069
|
+
} catch {
|
|
16070
|
+
}
|
|
16071
|
+
return results;
|
|
16072
|
+
}
|
|
16073
|
+
var EXCLUDE_FOLDERS = [
|
|
16074
|
+
"daily-notes",
|
|
16075
|
+
"daily",
|
|
16076
|
+
"weekly",
|
|
16077
|
+
"weekly-notes",
|
|
16078
|
+
"monthly",
|
|
16079
|
+
"monthly-notes",
|
|
16080
|
+
"quarterly",
|
|
16081
|
+
"yearly-notes",
|
|
16082
|
+
"periodic",
|
|
16083
|
+
"journal",
|
|
16084
|
+
"inbox",
|
|
16085
|
+
"templates",
|
|
16086
|
+
"attachments",
|
|
16087
|
+
"tmp",
|
|
16088
|
+
"clippings",
|
|
16089
|
+
"readwise",
|
|
16090
|
+
"articles",
|
|
16091
|
+
"bookmarks",
|
|
16092
|
+
"web-clips"
|
|
16093
|
+
];
|
|
16094
|
+
function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
16095
|
+
server2.tool(
|
|
16096
|
+
"vault_init",
|
|
16097
|
+
"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.",
|
|
16098
|
+
{
|
|
16099
|
+
dry_run: z23.boolean().default(true).describe("If true (default), preview what would be linked without modifying files"),
|
|
16100
|
+
batch_size: z23.number().default(50).describe("Maximum notes to process per invocation (default: 50)"),
|
|
16101
|
+
offset: z23.number().default(0).describe("Skip this many eligible notes (for pagination across invocations)")
|
|
16102
|
+
},
|
|
16103
|
+
async ({ dry_run, batch_size, offset }) => {
|
|
16104
|
+
const startTime = Date.now();
|
|
16105
|
+
const stateDb2 = getStateDb();
|
|
16106
|
+
const lastRunRow = stateDb2?.getMetadataValue.get("vault_init_last_run_at");
|
|
16107
|
+
const totalEnrichedRow = stateDb2?.getMetadataValue.get("vault_init_total_enriched");
|
|
16108
|
+
const previousTotal = totalEnrichedRow ? parseInt(totalEnrichedRow.value, 10) : 0;
|
|
16109
|
+
checkAndRefreshIfStale();
|
|
16110
|
+
if (!isEntityIndexReady()) {
|
|
16111
|
+
const result = {
|
|
16112
|
+
success: false,
|
|
16113
|
+
mode: dry_run ? "dry_run" : "apply",
|
|
16114
|
+
notes_scanned: 0,
|
|
16115
|
+
notes_with_matches: 0,
|
|
16116
|
+
notes_skipped: 0,
|
|
16117
|
+
total_matches: 0,
|
|
16118
|
+
preview: [],
|
|
16119
|
+
duration_ms: Date.now() - startTime,
|
|
16120
|
+
last_run_at: lastRunRow?.value ?? null,
|
|
16121
|
+
total_enriched: previousTotal
|
|
16122
|
+
};
|
|
16123
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...result, error: "Entity index not ready" }, null, 2) }] };
|
|
16124
|
+
}
|
|
16125
|
+
const allFiles = await collectMarkdownFiles(vaultPath2, vaultPath2, EXCLUDE_FOLDERS);
|
|
16126
|
+
const eligible = [];
|
|
16127
|
+
let notesSkipped = 0;
|
|
16128
|
+
for (const relativePath of allFiles) {
|
|
16129
|
+
const fullPath = path30.join(vaultPath2, relativePath);
|
|
16130
|
+
let content;
|
|
16131
|
+
try {
|
|
16132
|
+
content = await fs29.readFile(fullPath, "utf-8");
|
|
16133
|
+
} catch {
|
|
16134
|
+
continue;
|
|
16135
|
+
}
|
|
16136
|
+
if (hasSkipWikilinks(content)) {
|
|
16137
|
+
notesSkipped++;
|
|
16138
|
+
continue;
|
|
16139
|
+
}
|
|
16140
|
+
const existingLinks = extractLinkedEntities(content);
|
|
16141
|
+
if (existingLinks.size > 0) continue;
|
|
16142
|
+
eligible.push({ relativePath, content });
|
|
16143
|
+
}
|
|
16144
|
+
const paged = eligible.slice(offset, offset + batch_size);
|
|
16145
|
+
const preview = [];
|
|
16146
|
+
let totalMatches = 0;
|
|
16147
|
+
let notesModified = 0;
|
|
16148
|
+
for (const { relativePath, content } of paged) {
|
|
16149
|
+
const result = processWikilinks(content, relativePath);
|
|
16150
|
+
if (result.linksAdded === 0) continue;
|
|
16151
|
+
const entities = result.linkedEntities;
|
|
16152
|
+
totalMatches += result.linksAdded;
|
|
16153
|
+
preview.push({
|
|
16154
|
+
note: relativePath,
|
|
16155
|
+
entities,
|
|
16156
|
+
match_count: result.linksAdded
|
|
16157
|
+
});
|
|
16158
|
+
if (!dry_run) {
|
|
16159
|
+
const fullPath = path30.join(vaultPath2, relativePath);
|
|
16160
|
+
await fs29.writeFile(fullPath, result.content, "utf-8");
|
|
16161
|
+
notesModified++;
|
|
16162
|
+
if (stateDb2) {
|
|
16163
|
+
trackWikilinkApplications(stateDb2, relativePath, entities);
|
|
16164
|
+
const newLinks = extractLinkedEntities(result.content);
|
|
16165
|
+
updateStoredNoteLinks(stateDb2, relativePath, newLinks);
|
|
16166
|
+
}
|
|
16167
|
+
}
|
|
16168
|
+
}
|
|
16169
|
+
if (!dry_run && stateDb2 && notesModified > 0) {
|
|
16170
|
+
const newTotal = previousTotal + notesModified;
|
|
16171
|
+
stateDb2.setMetadataValue.run("vault_init_last_run_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
16172
|
+
stateDb2.setMetadataValue.run("vault_init_total_enriched", String(newTotal));
|
|
16173
|
+
}
|
|
16174
|
+
const currentLastRun = !dry_run && notesModified > 0 ? (/* @__PURE__ */ new Date()).toISOString() : lastRunRow?.value ?? null;
|
|
16175
|
+
const currentTotal = !dry_run ? previousTotal + notesModified : previousTotal;
|
|
16176
|
+
const output = {
|
|
16177
|
+
success: true,
|
|
16178
|
+
mode: dry_run ? "dry_run" : "apply",
|
|
16179
|
+
notes_scanned: allFiles.length,
|
|
16180
|
+
notes_with_matches: preview.length,
|
|
16181
|
+
notes_skipped: notesSkipped,
|
|
16182
|
+
total_matches: totalMatches,
|
|
16183
|
+
...dry_run ? {} : { notes_modified: notesModified },
|
|
16184
|
+
preview: preview.slice(0, 20),
|
|
16185
|
+
// Cap preview to 20 items in output
|
|
16186
|
+
duration_ms: Date.now() - startTime,
|
|
16187
|
+
last_run_at: currentLastRun,
|
|
16188
|
+
total_enriched: currentTotal
|
|
16189
|
+
};
|
|
16190
|
+
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
16191
|
+
}
|
|
16192
|
+
);
|
|
16193
|
+
}
|
|
16194
|
+
|
|
16195
|
+
// src/tools/read/metrics.ts
|
|
16196
|
+
import { z as z24 } from "zod";
|
|
15991
16197
|
|
|
15992
16198
|
// src/core/shared/metrics.ts
|
|
15993
16199
|
var ALL_METRICS = [
|
|
@@ -16153,10 +16359,10 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16153
16359
|
title: "Vault Growth",
|
|
16154
16360
|
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.',
|
|
16155
16361
|
inputSchema: {
|
|
16156
|
-
mode:
|
|
16157
|
-
metric:
|
|
16158
|
-
days_back:
|
|
16159
|
-
limit:
|
|
16362
|
+
mode: z24.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
|
|
16363
|
+
metric: z24.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
|
|
16364
|
+
days_back: z24.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
|
|
16365
|
+
limit: z24.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
|
|
16160
16366
|
}
|
|
16161
16367
|
},
|
|
16162
16368
|
async ({ mode, metric, days_back, limit: eventLimit }) => {
|
|
@@ -16229,7 +16435,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16229
16435
|
}
|
|
16230
16436
|
|
|
16231
16437
|
// src/tools/read/activity.ts
|
|
16232
|
-
import { z as
|
|
16438
|
+
import { z as z25 } from "zod";
|
|
16233
16439
|
|
|
16234
16440
|
// src/core/shared/toolTracking.ts
|
|
16235
16441
|
function recordToolInvocation(stateDb2, event) {
|
|
@@ -16309,8 +16515,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
|
16309
16515
|
}
|
|
16310
16516
|
}
|
|
16311
16517
|
}
|
|
16312
|
-
return Array.from(noteMap.entries()).map(([
|
|
16313
|
-
path:
|
|
16518
|
+
return Array.from(noteMap.entries()).map(([path33, stats]) => ({
|
|
16519
|
+
path: path33,
|
|
16314
16520
|
access_count: stats.access_count,
|
|
16315
16521
|
last_accessed: stats.last_accessed,
|
|
16316
16522
|
tools_used: Array.from(stats.tools)
|
|
@@ -16389,10 +16595,10 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
16389
16595
|
title: "Vault Activity",
|
|
16390
16596
|
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)',
|
|
16391
16597
|
inputSchema: {
|
|
16392
|
-
mode:
|
|
16393
|
-
session_id:
|
|
16394
|
-
days_back:
|
|
16395
|
-
limit:
|
|
16598
|
+
mode: z25.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
|
|
16599
|
+
session_id: z25.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
|
|
16600
|
+
days_back: z25.number().optional().describe("Number of days to look back (default: 30)"),
|
|
16601
|
+
limit: z25.number().optional().describe("Maximum results to return (default: 20)")
|
|
16396
16602
|
}
|
|
16397
16603
|
},
|
|
16398
16604
|
async ({ mode, session_id, days_back, limit: resultLimit }) => {
|
|
@@ -16459,11 +16665,11 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
16459
16665
|
}
|
|
16460
16666
|
|
|
16461
16667
|
// src/tools/read/similarity.ts
|
|
16462
|
-
import { z as
|
|
16668
|
+
import { z as z26 } from "zod";
|
|
16463
16669
|
|
|
16464
16670
|
// src/core/read/similarity.ts
|
|
16465
|
-
import * as
|
|
16466
|
-
import * as
|
|
16671
|
+
import * as fs30 from "fs";
|
|
16672
|
+
import * as path31 from "path";
|
|
16467
16673
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
16468
16674
|
"the",
|
|
16469
16675
|
"be",
|
|
@@ -16600,10 +16806,10 @@ function extractKeyTerms(content, maxTerms = 15) {
|
|
|
16600
16806
|
}
|
|
16601
16807
|
function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
16602
16808
|
const limit = options.limit ?? 10;
|
|
16603
|
-
const absPath =
|
|
16809
|
+
const absPath = path31.join(vaultPath2, sourcePath);
|
|
16604
16810
|
let content;
|
|
16605
16811
|
try {
|
|
16606
|
-
content =
|
|
16812
|
+
content = fs30.readFileSync(absPath, "utf-8");
|
|
16607
16813
|
} catch {
|
|
16608
16814
|
return [];
|
|
16609
16815
|
}
|
|
@@ -16723,12 +16929,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16723
16929
|
title: "Find Similar Notes",
|
|
16724
16930
|
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.",
|
|
16725
16931
|
inputSchema: {
|
|
16726
|
-
path:
|
|
16727
|
-
limit:
|
|
16728
|
-
exclude_linked:
|
|
16932
|
+
path: z26.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
|
|
16933
|
+
limit: z26.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
|
|
16934
|
+
exclude_linked: z26.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
|
|
16729
16935
|
}
|
|
16730
16936
|
},
|
|
16731
|
-
async ({ path:
|
|
16937
|
+
async ({ path: path33, limit, exclude_linked }) => {
|
|
16732
16938
|
const index = getIndex();
|
|
16733
16939
|
const vaultPath2 = getVaultPath();
|
|
16734
16940
|
const stateDb2 = getStateDb();
|
|
@@ -16737,10 +16943,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16737
16943
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
16738
16944
|
};
|
|
16739
16945
|
}
|
|
16740
|
-
if (!index.notes.has(
|
|
16946
|
+
if (!index.notes.has(path33)) {
|
|
16741
16947
|
return {
|
|
16742
16948
|
content: [{ type: "text", text: JSON.stringify({
|
|
16743
|
-
error: `Note not found: ${
|
|
16949
|
+
error: `Note not found: ${path33}`,
|
|
16744
16950
|
hint: "Use the full relative path including .md extension"
|
|
16745
16951
|
}, null, 2) }]
|
|
16746
16952
|
};
|
|
@@ -16751,12 +16957,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16751
16957
|
};
|
|
16752
16958
|
const useHybrid = hasEmbeddingsIndex();
|
|
16753
16959
|
const method = useHybrid ? "hybrid" : "bm25";
|
|
16754
|
-
const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index,
|
|
16960
|
+
const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path33, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path33, opts);
|
|
16755
16961
|
return {
|
|
16756
16962
|
content: [{
|
|
16757
16963
|
type: "text",
|
|
16758
16964
|
text: JSON.stringify({
|
|
16759
|
-
source:
|
|
16965
|
+
source: path33,
|
|
16760
16966
|
method,
|
|
16761
16967
|
exclude_linked: exclude_linked ?? true,
|
|
16762
16968
|
count: results.length,
|
|
@@ -16769,7 +16975,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16769
16975
|
}
|
|
16770
16976
|
|
|
16771
16977
|
// src/tools/read/semantic.ts
|
|
16772
|
-
import { z as
|
|
16978
|
+
import { z as z27 } from "zod";
|
|
16773
16979
|
import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
|
|
16774
16980
|
function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
16775
16981
|
server2.registerTool(
|
|
@@ -16778,7 +16984,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
16778
16984
|
title: "Initialize Semantic Search",
|
|
16779
16985
|
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.",
|
|
16780
16986
|
inputSchema: {
|
|
16781
|
-
force:
|
|
16987
|
+
force: z27.boolean().optional().describe(
|
|
16782
16988
|
"Rebuild all embeddings even if they already exist (default: false)"
|
|
16783
16989
|
)
|
|
16784
16990
|
}
|
|
@@ -16858,7 +17064,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
16858
17064
|
|
|
16859
17065
|
// src/tools/read/merges.ts
|
|
16860
17066
|
init_levenshtein();
|
|
16861
|
-
import { z as
|
|
17067
|
+
import { z as z28 } from "zod";
|
|
16862
17068
|
import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
|
|
16863
17069
|
function normalizeName(name) {
|
|
16864
17070
|
return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
|
|
@@ -16868,7 +17074,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
16868
17074
|
"suggest_entity_merges",
|
|
16869
17075
|
"Find potential duplicate entities that could be merged based on name similarity",
|
|
16870
17076
|
{
|
|
16871
|
-
limit:
|
|
17077
|
+
limit: z28.number().optional().default(50).describe("Maximum number of suggestions to return")
|
|
16872
17078
|
},
|
|
16873
17079
|
async ({ limit }) => {
|
|
16874
17080
|
const stateDb2 = getStateDb();
|
|
@@ -16970,11 +17176,11 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
16970
17176
|
"dismiss_merge_suggestion",
|
|
16971
17177
|
"Permanently dismiss a merge suggestion so it never reappears",
|
|
16972
17178
|
{
|
|
16973
|
-
source_path:
|
|
16974
|
-
target_path:
|
|
16975
|
-
source_name:
|
|
16976
|
-
target_name:
|
|
16977
|
-
reason:
|
|
17179
|
+
source_path: z28.string().describe("Path of the source entity"),
|
|
17180
|
+
target_path: z28.string().describe("Path of the target entity"),
|
|
17181
|
+
source_name: z28.string().describe("Name of the source entity"),
|
|
17182
|
+
target_name: z28.string().describe("Name of the target entity"),
|
|
17183
|
+
reason: z28.string().describe("Original suggestion reason")
|
|
16978
17184
|
},
|
|
16979
17185
|
async ({ source_path, target_path, source_name, target_name, reason }) => {
|
|
16980
17186
|
const stateDb2 = getStateDb();
|
|
@@ -16993,7 +17199,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
16993
17199
|
}
|
|
16994
17200
|
|
|
16995
17201
|
// src/index.ts
|
|
16996
|
-
import * as
|
|
17202
|
+
import * as fs31 from "node:fs/promises";
|
|
16997
17203
|
import { createHash as createHash2 } from "node:crypto";
|
|
16998
17204
|
|
|
16999
17205
|
// src/resources/vault.ts
|
|
@@ -17102,7 +17308,7 @@ function registerVaultResources(server2, getIndex) {
|
|
|
17102
17308
|
// src/index.ts
|
|
17103
17309
|
var __filename = fileURLToPath(import.meta.url);
|
|
17104
17310
|
var __dirname = dirname4(__filename);
|
|
17105
|
-
var pkg = JSON.parse(readFileSync4(
|
|
17311
|
+
var pkg = JSON.parse(readFileSync4(join17(__dirname, "../package.json"), "utf-8"));
|
|
17106
17312
|
var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
17107
17313
|
var resolvedVaultPath;
|
|
17108
17314
|
try {
|
|
@@ -17369,6 +17575,7 @@ registerSystemTools2(server, vaultPath);
|
|
|
17369
17575
|
registerPolicyTools(server, vaultPath);
|
|
17370
17576
|
registerTagTools(server, () => vaultIndex, () => vaultPath);
|
|
17371
17577
|
registerWikilinkFeedbackTools(server, () => stateDb);
|
|
17578
|
+
registerInitTools(server, vaultPath, () => stateDb);
|
|
17372
17579
|
registerConfigTools(
|
|
17373
17580
|
server,
|
|
17374
17581
|
() => flywheelConfig,
|
|
@@ -17406,6 +17613,10 @@ async function main() {
|
|
|
17406
17613
|
setWriteStateDb(stateDb);
|
|
17407
17614
|
setRecencyStateDb(stateDb);
|
|
17408
17615
|
setEdgeWeightStateDb(stateDb);
|
|
17616
|
+
const vaultInitRow = stateDb.getMetadataValue.get("vault_init_last_run_at");
|
|
17617
|
+
if (!vaultInitRow) {
|
|
17618
|
+
serverLog("server", "Vault not initialized \u2014 call vault_init to enrich legacy notes");
|
|
17619
|
+
}
|
|
17409
17620
|
} catch (err) {
|
|
17410
17621
|
const msg = err instanceof Error ? err.message : String(err);
|
|
17411
17622
|
serverLog("statedb", `StateDb initialization failed: ${msg}`, "error");
|
|
@@ -17523,22 +17734,22 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
|
17523
17734
|
async function scanDir(dir) {
|
|
17524
17735
|
let entries;
|
|
17525
17736
|
try {
|
|
17526
|
-
entries = await
|
|
17737
|
+
entries = await fs31.readdir(dir, { withFileTypes: true });
|
|
17527
17738
|
} catch {
|
|
17528
17739
|
return;
|
|
17529
17740
|
}
|
|
17530
17741
|
for (const entry of entries) {
|
|
17531
|
-
const fullPath =
|
|
17742
|
+
const fullPath = path32.join(dir, entry.name);
|
|
17532
17743
|
if (entry.isDirectory()) {
|
|
17533
17744
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
17534
17745
|
await scanDir(fullPath);
|
|
17535
17746
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
17536
17747
|
try {
|
|
17537
|
-
const stat4 = await
|
|
17748
|
+
const stat4 = await fs31.stat(fullPath);
|
|
17538
17749
|
if (stat4.mtimeMs > sinceMs) {
|
|
17539
17750
|
events.push({
|
|
17540
17751
|
type: "upsert",
|
|
17541
|
-
path:
|
|
17752
|
+
path: path32.relative(vaultPath2, fullPath),
|
|
17542
17753
|
originalEvents: []
|
|
17543
17754
|
});
|
|
17544
17755
|
}
|
|
@@ -17663,8 +17874,8 @@ async function runPostIndexWork(index) {
|
|
|
17663
17874
|
}
|
|
17664
17875
|
} catch {
|
|
17665
17876
|
try {
|
|
17666
|
-
const dir =
|
|
17667
|
-
const base =
|
|
17877
|
+
const dir = path32.dirname(rawPath);
|
|
17878
|
+
const base = path32.basename(rawPath);
|
|
17668
17879
|
const resolvedDir = realpathSync(dir).replace(/\\/g, "/");
|
|
17669
17880
|
for (const prefix of vaultPrefixes) {
|
|
17670
17881
|
if (resolvedDir.startsWith(prefix + "/") || resolvedDir === prefix) {
|
|
@@ -17693,7 +17904,7 @@ async function runPostIndexWork(index) {
|
|
|
17693
17904
|
continue;
|
|
17694
17905
|
}
|
|
17695
17906
|
try {
|
|
17696
|
-
const content = await
|
|
17907
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
17697
17908
|
const hash = createHash2("md5").update(content).digest("hex");
|
|
17698
17909
|
if (lastContentHashes.get(event.path) === hash) {
|
|
17699
17910
|
serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
|
|
@@ -17764,7 +17975,7 @@ async function runPostIndexWork(index) {
|
|
|
17764
17975
|
...batch,
|
|
17765
17976
|
events: filteredEvents.map((e) => ({
|
|
17766
17977
|
...e,
|
|
17767
|
-
path:
|
|
17978
|
+
path: path32.join(vaultPath, e.path)
|
|
17768
17979
|
}))
|
|
17769
17980
|
};
|
|
17770
17981
|
const batchResult = await processBatch(vaultIndex, vaultPath, absoluteBatch);
|
|
@@ -17869,7 +18080,15 @@ async function runPostIndexWork(index) {
|
|
|
17869
18080
|
if (edgeWeightAgeMs >= 60 * 60 * 1e3) {
|
|
17870
18081
|
const result = recomputeEdgeWeights(stateDb);
|
|
17871
18082
|
lastEdgeWeightRebuildAt = Date.now();
|
|
17872
|
-
tracker.end({
|
|
18083
|
+
tracker.end({
|
|
18084
|
+
rebuilt: true,
|
|
18085
|
+
edges: result.edges_updated,
|
|
18086
|
+
duration_ms: result.duration_ms,
|
|
18087
|
+
total_weighted: result.total_weighted,
|
|
18088
|
+
avg_weight: result.avg_weight,
|
|
18089
|
+
strong_count: result.strong_count,
|
|
18090
|
+
top_changes: result.top_changes
|
|
18091
|
+
});
|
|
17873
18092
|
serverLog("watcher", `Edge weights: ${result.edges_updated} edges in ${result.duration_ms}ms`);
|
|
17874
18093
|
} else {
|
|
17875
18094
|
tracker.end({ rebuilt: false, age_ms: edgeWeightAgeMs });
|
|
@@ -17890,7 +18109,7 @@ async function runPostIndexWork(index) {
|
|
|
17890
18109
|
removeEmbedding(event.path);
|
|
17891
18110
|
embRemoved++;
|
|
17892
18111
|
} else if (event.path.endsWith(".md")) {
|
|
17893
|
-
const absPath =
|
|
18112
|
+
const absPath = path32.join(vaultPath, event.path);
|
|
17894
18113
|
await updateEmbedding(event.path, absPath);
|
|
17895
18114
|
embUpdated++;
|
|
17896
18115
|
}
|
|
@@ -18097,7 +18316,7 @@ async function runPostIndexWork(index) {
|
|
|
18097
18316
|
for (const event of filteredEvents) {
|
|
18098
18317
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
18099
18318
|
try {
|
|
18100
|
-
const content = await
|
|
18319
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
18101
18320
|
const zones = getProtectedZones2(content);
|
|
18102
18321
|
const linked = new Set(
|
|
18103
18322
|
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).map((n) => n.toLowerCase())
|
|
@@ -18133,7 +18352,7 @@ async function runPostIndexWork(index) {
|
|
|
18133
18352
|
for (const event of filteredEvents) {
|
|
18134
18353
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
18135
18354
|
try {
|
|
18136
|
-
const content = await
|
|
18355
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
18137
18356
|
const removed = processImplicitFeedback(stateDb, event.path, content);
|
|
18138
18357
|
for (const entity of removed) feedbackResults.push({ entity, file: event.path });
|
|
18139
18358
|
} catch {
|
|
@@ -18154,9 +18373,27 @@ async function runPostIndexWork(index) {
|
|
|
18154
18373
|
}
|
|
18155
18374
|
}
|
|
18156
18375
|
}
|
|
18157
|
-
|
|
18158
|
-
if (
|
|
18159
|
-
|
|
18376
|
+
const additionResults = [];
|
|
18377
|
+
if (stateDb && linkDiffs.length > 0) {
|
|
18378
|
+
const checkApplication = stateDb.db.prepare(
|
|
18379
|
+
`SELECT 1 FROM wikilink_applications WHERE LOWER(entity) = LOWER(?) AND note_path = ? AND status = 'applied'`
|
|
18380
|
+
);
|
|
18381
|
+
for (const diff of linkDiffs) {
|
|
18382
|
+
for (const target of diff.added) {
|
|
18383
|
+
if (checkApplication.get(target, diff.file)) continue;
|
|
18384
|
+
const entity = entitiesAfter.find(
|
|
18385
|
+
(e) => e.nameLower === target || (e.aliases ?? []).some((a) => a.toLowerCase() === target)
|
|
18386
|
+
);
|
|
18387
|
+
if (entity) {
|
|
18388
|
+
recordFeedback(stateDb, entity.name, "implicit:manual_added", diff.file, true);
|
|
18389
|
+
additionResults.push({ entity: entity.name, file: diff.file });
|
|
18390
|
+
}
|
|
18391
|
+
}
|
|
18392
|
+
}
|
|
18393
|
+
}
|
|
18394
|
+
tracker.end({ removals: feedbackResults, additions: additionResults });
|
|
18395
|
+
if (feedbackResults.length > 0 || additionResults.length > 0) {
|
|
18396
|
+
serverLog("watcher", `Implicit feedback: ${feedbackResults.length} removals, ${additionResults.length} manual additions detected`);
|
|
18160
18397
|
}
|
|
18161
18398
|
tracker.start("tag_scan", { files: filteredEvents.length });
|
|
18162
18399
|
const tagDiffs = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.45",
|
|
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.44",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|