@velvetmonkey/flywheel-memory 2.0.32 → 2.0.34
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 +479 -37
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2486,7 +2486,11 @@ import {
|
|
|
2486
2486
|
var DEFAULT_CONFIG = {
|
|
2487
2487
|
exclude_task_tags: [],
|
|
2488
2488
|
exclude_analysis_tags: [],
|
|
2489
|
-
exclude_entities: []
|
|
2489
|
+
exclude_entities: [],
|
|
2490
|
+
exclude_entity_folders: [],
|
|
2491
|
+
wikilink_strictness: "balanced",
|
|
2492
|
+
implicit_detection: true,
|
|
2493
|
+
adaptive_strictness: true
|
|
2490
2494
|
};
|
|
2491
2495
|
function loadConfig(stateDb2) {
|
|
2492
2496
|
if (stateDb2) {
|
|
@@ -3403,10 +3407,10 @@ function getAllFeedbackBoosts(stateDb2, folder) {
|
|
|
3403
3407
|
for (const row of globalRows) {
|
|
3404
3408
|
let accuracy;
|
|
3405
3409
|
let sampleCount;
|
|
3406
|
-
const
|
|
3407
|
-
if (
|
|
3408
|
-
accuracy =
|
|
3409
|
-
sampleCount =
|
|
3410
|
+
const fs31 = folderStats?.get(row.entity);
|
|
3411
|
+
if (fs31 && fs31.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
|
|
3412
|
+
accuracy = fs31.accuracy;
|
|
3413
|
+
sampleCount = fs31.count;
|
|
3410
3414
|
} else {
|
|
3411
3415
|
accuracy = row.correct_count / row.total;
|
|
3412
3416
|
sampleCount = row.total;
|
|
@@ -4901,6 +4905,24 @@ function setWriteStateDb(stateDb2) {
|
|
|
4901
4905
|
function getWriteStateDb() {
|
|
4902
4906
|
return moduleStateDb4;
|
|
4903
4907
|
}
|
|
4908
|
+
var moduleConfig = null;
|
|
4909
|
+
var ALL_IMPLICIT_PATTERNS = ["proper-nouns", "single-caps", "quoted-terms", "camel-case", "acronyms"];
|
|
4910
|
+
function setWikilinkConfig(config) {
|
|
4911
|
+
moduleConfig = config;
|
|
4912
|
+
}
|
|
4913
|
+
function getWikilinkStrictness() {
|
|
4914
|
+
return moduleConfig?.wikilink_strictness ?? "balanced";
|
|
4915
|
+
}
|
|
4916
|
+
function getEffectiveStrictness(notePath) {
|
|
4917
|
+
const base = getWikilinkStrictness();
|
|
4918
|
+
if (moduleConfig?.adaptive_strictness === false) return base;
|
|
4919
|
+
const context = notePath ? getNoteContext(notePath) : "general";
|
|
4920
|
+
if (context === "daily") return "aggressive";
|
|
4921
|
+
return base;
|
|
4922
|
+
}
|
|
4923
|
+
function getCooccurrenceIndex() {
|
|
4924
|
+
return cooccurrenceIndex;
|
|
4925
|
+
}
|
|
4904
4926
|
var entityIndex = null;
|
|
4905
4927
|
var indexReady = false;
|
|
4906
4928
|
var indexError2 = null;
|
|
@@ -5061,9 +5083,11 @@ function processWikilinks(content, notePath) {
|
|
|
5061
5083
|
firstOccurrenceOnly: true,
|
|
5062
5084
|
caseInsensitive: true
|
|
5063
5085
|
});
|
|
5086
|
+
const implicitEnabled = moduleConfig?.implicit_detection !== false;
|
|
5087
|
+
const implicitPatterns = moduleConfig?.implicit_patterns?.length ? moduleConfig.implicit_patterns : [...ALL_IMPLICIT_PATTERNS];
|
|
5064
5088
|
const implicitMatches = detectImplicitEntities(result.content, {
|
|
5065
|
-
detectImplicit:
|
|
5066
|
-
implicitPatterns
|
|
5089
|
+
detectImplicit: implicitEnabled,
|
|
5090
|
+
implicitPatterns,
|
|
5067
5091
|
minEntityLength: 3
|
|
5068
5092
|
});
|
|
5069
5093
|
const alreadyLinked = new Set(
|
|
@@ -5078,12 +5102,22 @@ function processWikilinks(content, notePath) {
|
|
|
5078
5102
|
}
|
|
5079
5103
|
}
|
|
5080
5104
|
const currentNoteName = notePath ? notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() : null;
|
|
5081
|
-
|
|
5105
|
+
let newImplicits = implicitMatches.filter((m) => {
|
|
5082
5106
|
const normalized = m.text.toLowerCase();
|
|
5083
5107
|
if (alreadyLinked.has(normalized)) return false;
|
|
5084
5108
|
if (currentNoteName && normalized === currentNoteName) return false;
|
|
5085
5109
|
return true;
|
|
5086
5110
|
});
|
|
5111
|
+
const nonOverlapping = [];
|
|
5112
|
+
for (const match of newImplicits) {
|
|
5113
|
+
const overlaps = nonOverlapping.some(
|
|
5114
|
+
(existing) => match.start >= existing.start && match.start < existing.end || match.end > existing.start && match.end <= existing.end || match.start <= existing.start && match.end >= existing.end
|
|
5115
|
+
);
|
|
5116
|
+
if (!overlaps) {
|
|
5117
|
+
nonOverlapping.push(match);
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
newImplicits = nonOverlapping;
|
|
5087
5121
|
if (newImplicits.length > 0) {
|
|
5088
5122
|
let processedContent = result.content;
|
|
5089
5123
|
for (let i = newImplicits.length - 1; i >= 0; i--) {
|
|
@@ -5283,7 +5317,6 @@ var STRICTNESS_CONFIGS = {
|
|
|
5283
5317
|
// Standard bonus for exact matches
|
|
5284
5318
|
}
|
|
5285
5319
|
};
|
|
5286
|
-
var DEFAULT_STRICTNESS = "conservative";
|
|
5287
5320
|
var TYPE_BOOST = {
|
|
5288
5321
|
people: 5,
|
|
5289
5322
|
// Names are high value for connections
|
|
@@ -5470,7 +5503,7 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
5470
5503
|
const {
|
|
5471
5504
|
maxSuggestions = 3,
|
|
5472
5505
|
excludeLinked = true,
|
|
5473
|
-
strictness =
|
|
5506
|
+
strictness = getEffectiveStrictness(options.notePath),
|
|
5474
5507
|
notePath,
|
|
5475
5508
|
detail = false
|
|
5476
5509
|
} = options;
|
|
@@ -6035,6 +6068,17 @@ function searchFTS5(_vaultPath, query, limit = 10) {
|
|
|
6035
6068
|
function getFTS5State() {
|
|
6036
6069
|
return { ...state };
|
|
6037
6070
|
}
|
|
6071
|
+
function countFTS5Mentions(term) {
|
|
6072
|
+
if (!db2) return 0;
|
|
6073
|
+
try {
|
|
6074
|
+
const result = db2.prepare(
|
|
6075
|
+
"SELECT COUNT(*) as cnt FROM notes_fts WHERE content MATCH ?"
|
|
6076
|
+
).get(`"${term}"`);
|
|
6077
|
+
return result?.cnt ?? 0;
|
|
6078
|
+
} catch {
|
|
6079
|
+
return 0;
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6038
6082
|
|
|
6039
6083
|
// src/core/read/taskCache.ts
|
|
6040
6084
|
import * as path10 from "path";
|
|
@@ -7046,12 +7090,13 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
7046
7090
|
inputSchema: {
|
|
7047
7091
|
path: z2.string().optional().describe("Path to a specific note to validate. If omitted, validates all notes."),
|
|
7048
7092
|
typos_only: z2.boolean().default(false).describe("If true, only report broken links that have a similar existing note (likely typos)"),
|
|
7093
|
+
group_by_target: z2.boolean().default(false).describe("If true, aggregate dead links by target and rank by mention frequency. Returns targets[] instead of broken[]."),
|
|
7049
7094
|
limit: z2.coerce.number().default(50).describe("Maximum number of broken links to return"),
|
|
7050
7095
|
offset: z2.coerce.number().default(0).describe("Number of broken links to skip (for pagination)")
|
|
7051
7096
|
},
|
|
7052
7097
|
outputSchema: ValidateLinksOutputSchema
|
|
7053
7098
|
},
|
|
7054
|
-
async ({ path: notePath, typos_only, limit: requestedLimit, offset }) => {
|
|
7099
|
+
async ({ path: notePath, typos_only, group_by_target, limit: requestedLimit, offset }) => {
|
|
7055
7100
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
7056
7101
|
const index = getIndex();
|
|
7057
7102
|
const allBroken = [];
|
|
@@ -7092,6 +7137,41 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
7092
7137
|
}
|
|
7093
7138
|
}
|
|
7094
7139
|
}
|
|
7140
|
+
if (group_by_target) {
|
|
7141
|
+
const targetMap = /* @__PURE__ */ new Map();
|
|
7142
|
+
for (const broken2 of allBroken) {
|
|
7143
|
+
const key = broken2.target.toLowerCase();
|
|
7144
|
+
const existing = targetMap.get(key);
|
|
7145
|
+
if (existing) {
|
|
7146
|
+
existing.count++;
|
|
7147
|
+
if (existing.sources.size < 5) existing.sources.add(broken2.source);
|
|
7148
|
+
if (!existing.suggestion && broken2.suggestion) existing.suggestion = broken2.suggestion;
|
|
7149
|
+
} else {
|
|
7150
|
+
targetMap.set(key, {
|
|
7151
|
+
count: 1,
|
|
7152
|
+
sources: /* @__PURE__ */ new Set([broken2.source]),
|
|
7153
|
+
suggestion: broken2.suggestion
|
|
7154
|
+
});
|
|
7155
|
+
}
|
|
7156
|
+
}
|
|
7157
|
+
const targets = Array.from(targetMap.entries()).map(([target, data]) => ({
|
|
7158
|
+
target,
|
|
7159
|
+
mention_count: data.count,
|
|
7160
|
+
sources: Array.from(data.sources),
|
|
7161
|
+
...data.suggestion ? { suggestion: data.suggestion } : {}
|
|
7162
|
+
})).sort((a, b) => b.mention_count - a.mention_count).slice(offset, offset + limit);
|
|
7163
|
+
const grouped = {
|
|
7164
|
+
scope: notePath || "all",
|
|
7165
|
+
total_dead_targets: targetMap.size,
|
|
7166
|
+
total_broken_links: allBroken.length,
|
|
7167
|
+
returned_count: targets.length,
|
|
7168
|
+
targets
|
|
7169
|
+
};
|
|
7170
|
+
return {
|
|
7171
|
+
content: [{ type: "text", text: JSON.stringify(grouped, null, 2) }],
|
|
7172
|
+
structuredContent: grouped
|
|
7173
|
+
};
|
|
7174
|
+
}
|
|
7095
7175
|
const broken = allBroken.slice(offset, offset + limit);
|
|
7096
7176
|
const output = {
|
|
7097
7177
|
scope: notePath || "all",
|
|
@@ -7112,6 +7192,106 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
7112
7192
|
};
|
|
7113
7193
|
}
|
|
7114
7194
|
);
|
|
7195
|
+
server2.registerTool(
|
|
7196
|
+
"discover_stub_candidates",
|
|
7197
|
+
{
|
|
7198
|
+
title: "Discover Stub Candidates",
|
|
7199
|
+
description: `Find terms referenced via dead wikilinks across the vault that have no backing note. These are "invisible concepts" \u2014 topics your vault considers important enough to link to but that don't have their own notes yet. Ranked by reference frequency.`,
|
|
7200
|
+
inputSchema: {
|
|
7201
|
+
min_frequency: z2.coerce.number().default(2).describe("Minimum number of references to include (default 2)"),
|
|
7202
|
+
limit: z2.coerce.number().default(20).describe("Maximum candidates to return (default 20)")
|
|
7203
|
+
}
|
|
7204
|
+
},
|
|
7205
|
+
async ({ min_frequency, limit: requestedLimit }) => {
|
|
7206
|
+
const index = getIndex();
|
|
7207
|
+
const limit = Math.min(requestedLimit ?? 20, 100);
|
|
7208
|
+
const minFreq = min_frequency ?? 2;
|
|
7209
|
+
const targetMap = /* @__PURE__ */ new Map();
|
|
7210
|
+
for (const note of index.notes.values()) {
|
|
7211
|
+
for (const link of note.outlinks) {
|
|
7212
|
+
if (!resolveTarget(index, link.target)) {
|
|
7213
|
+
const key = link.target.toLowerCase();
|
|
7214
|
+
const existing = targetMap.get(key);
|
|
7215
|
+
if (existing) {
|
|
7216
|
+
existing.count++;
|
|
7217
|
+
if (existing.sources.size < 3) existing.sources.add(note.path);
|
|
7218
|
+
} else {
|
|
7219
|
+
targetMap.set(key, { count: 1, sources: /* @__PURE__ */ new Set([note.path]) });
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7224
|
+
const candidates = Array.from(targetMap.entries()).filter(([, data]) => data.count >= minFreq).map(([target, data]) => {
|
|
7225
|
+
const fts5Mentions = countFTS5Mentions(target);
|
|
7226
|
+
return {
|
|
7227
|
+
term: target,
|
|
7228
|
+
wikilink_references: data.count,
|
|
7229
|
+
content_mentions: fts5Mentions,
|
|
7230
|
+
sample_notes: Array.from(data.sources)
|
|
7231
|
+
};
|
|
7232
|
+
}).sort((a, b) => b.wikilink_references - a.wikilink_references).slice(0, limit);
|
|
7233
|
+
const output = {
|
|
7234
|
+
total_dead_targets: targetMap.size,
|
|
7235
|
+
candidates_above_threshold: candidates.length,
|
|
7236
|
+
candidates
|
|
7237
|
+
};
|
|
7238
|
+
return {
|
|
7239
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
7240
|
+
};
|
|
7241
|
+
}
|
|
7242
|
+
);
|
|
7243
|
+
server2.registerTool(
|
|
7244
|
+
"discover_cooccurrence_gaps",
|
|
7245
|
+
{
|
|
7246
|
+
title: "Discover Co-occurrence Gaps",
|
|
7247
|
+
description: "Find entity pairs that frequently co-occur across vault notes but where one or both entities lack a backing note. These represent relationship patterns worth making explicit with hub notes or links.",
|
|
7248
|
+
inputSchema: {
|
|
7249
|
+
min_cooccurrence: z2.coerce.number().default(3).describe("Minimum co-occurrence count to include (default 3)"),
|
|
7250
|
+
limit: z2.coerce.number().default(20).describe("Maximum gaps to return (default 20)")
|
|
7251
|
+
}
|
|
7252
|
+
},
|
|
7253
|
+
async ({ min_cooccurrence, limit: requestedLimit }) => {
|
|
7254
|
+
const index = getIndex();
|
|
7255
|
+
const coocIndex = getCooccurrenceIndex();
|
|
7256
|
+
const limit = Math.min(requestedLimit ?? 20, 100);
|
|
7257
|
+
const minCount = min_cooccurrence ?? 3;
|
|
7258
|
+
if (!coocIndex) {
|
|
7259
|
+
return {
|
|
7260
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Co-occurrence index not built yet. Wait for entity index initialization." }) }]
|
|
7261
|
+
};
|
|
7262
|
+
}
|
|
7263
|
+
const gaps = [];
|
|
7264
|
+
const seenPairs = /* @__PURE__ */ new Set();
|
|
7265
|
+
for (const [entityA, associations] of Object.entries(coocIndex.associations)) {
|
|
7266
|
+
for (const [entityB, count] of associations) {
|
|
7267
|
+
if (count < minCount) continue;
|
|
7268
|
+
const pairKey = [entityA, entityB].sort().join("||");
|
|
7269
|
+
if (seenPairs.has(pairKey)) continue;
|
|
7270
|
+
seenPairs.add(pairKey);
|
|
7271
|
+
const aHasNote = resolveTarget(index, entityA) !== null;
|
|
7272
|
+
const bHasNote = resolveTarget(index, entityB) !== null;
|
|
7273
|
+
if (aHasNote && bHasNote) continue;
|
|
7274
|
+
gaps.push({
|
|
7275
|
+
entity_a: entityA,
|
|
7276
|
+
entity_b: entityB,
|
|
7277
|
+
cooccurrence_count: count,
|
|
7278
|
+
a_has_note: aHasNote,
|
|
7279
|
+
b_has_note: bHasNote
|
|
7280
|
+
});
|
|
7281
|
+
}
|
|
7282
|
+
}
|
|
7283
|
+
gaps.sort((a, b) => b.cooccurrence_count - a.cooccurrence_count);
|
|
7284
|
+
const top = gaps.slice(0, limit);
|
|
7285
|
+
const output = {
|
|
7286
|
+
total_gaps: gaps.length,
|
|
7287
|
+
returned_count: top.length,
|
|
7288
|
+
gaps: top
|
|
7289
|
+
};
|
|
7290
|
+
return {
|
|
7291
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
7292
|
+
};
|
|
7293
|
+
}
|
|
7294
|
+
);
|
|
7115
7295
|
}
|
|
7116
7296
|
|
|
7117
7297
|
// src/tools/read/health.ts
|
|
@@ -7414,7 +7594,7 @@ function computeEntityDiff(before, after) {
|
|
|
7414
7594
|
const alias_changes = [];
|
|
7415
7595
|
for (const [key, entity] of afterMap) {
|
|
7416
7596
|
if (!beforeMap.has(key)) {
|
|
7417
|
-
added.push(entity.name);
|
|
7597
|
+
added.push({ name: entity.name, category: entity.category, path: entity.path });
|
|
7418
7598
|
} else {
|
|
7419
7599
|
const prev = beforeMap.get(key);
|
|
7420
7600
|
const prevAliases = JSON.stringify(prev.aliases.sort());
|
|
@@ -7426,7 +7606,7 @@ function computeEntityDiff(before, after) {
|
|
|
7426
7606
|
}
|
|
7427
7607
|
for (const [key, entity] of beforeMap) {
|
|
7428
7608
|
if (!afterMap.has(key)) {
|
|
7429
|
-
removed.push(entity.name);
|
|
7609
|
+
removed.push({ name: entity.name, category: entity.category, path: entity.path });
|
|
7430
7610
|
}
|
|
7431
7611
|
}
|
|
7432
7612
|
return { added, removed, alias_changes };
|
|
@@ -7477,6 +7657,93 @@ function purgeOldIndexEvents(stateDb2, retentionDays = 90) {
|
|
|
7477
7657
|
return result.changes;
|
|
7478
7658
|
}
|
|
7479
7659
|
|
|
7660
|
+
// src/core/read/sweep.ts
|
|
7661
|
+
var DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
7662
|
+
var MIN_SWEEP_INTERVAL_MS = 30 * 1e3;
|
|
7663
|
+
var cachedResults = null;
|
|
7664
|
+
var sweepTimer = null;
|
|
7665
|
+
var sweepRunning = false;
|
|
7666
|
+
function runSweep(index) {
|
|
7667
|
+
const start = Date.now();
|
|
7668
|
+
let deadLinkCount = 0;
|
|
7669
|
+
const deadTargetCounts = /* @__PURE__ */ new Map();
|
|
7670
|
+
for (const note of index.notes.values()) {
|
|
7671
|
+
for (const link of note.outlinks) {
|
|
7672
|
+
if (!resolveTarget(index, link.target)) {
|
|
7673
|
+
deadLinkCount++;
|
|
7674
|
+
const key = link.target.toLowerCase();
|
|
7675
|
+
deadTargetCounts.set(key, (deadTargetCounts.get(key) || 0) + 1);
|
|
7676
|
+
}
|
|
7677
|
+
}
|
|
7678
|
+
}
|
|
7679
|
+
const topDeadTargets = Array.from(deadTargetCounts.entries()).filter(([, count]) => count >= 2).map(([target, wikilink_references]) => ({
|
|
7680
|
+
target,
|
|
7681
|
+
wikilink_references,
|
|
7682
|
+
content_mentions: countFTS5Mentions(target)
|
|
7683
|
+
})).sort((a, b) => b.wikilink_references - a.wikilink_references).slice(0, 10);
|
|
7684
|
+
const linkedCounts = /* @__PURE__ */ new Map();
|
|
7685
|
+
for (const note of index.notes.values()) {
|
|
7686
|
+
for (const link of note.outlinks) {
|
|
7687
|
+
const key = link.target.toLowerCase();
|
|
7688
|
+
linkedCounts.set(key, (linkedCounts.get(key) || 0) + 1);
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7692
|
+
const unlinkedEntities = [];
|
|
7693
|
+
for (const [name, entityPath] of index.entities) {
|
|
7694
|
+
if (seen.has(entityPath)) continue;
|
|
7695
|
+
seen.add(entityPath);
|
|
7696
|
+
const totalMentions = countFTS5Mentions(name);
|
|
7697
|
+
if (totalMentions === 0) continue;
|
|
7698
|
+
const pathKey = entityPath.toLowerCase().replace(/\.md$/, "");
|
|
7699
|
+
const linked = Math.max(linkedCounts.get(name) || 0, linkedCounts.get(pathKey) || 0);
|
|
7700
|
+
const unlinked = Math.max(0, totalMentions - linked - 1);
|
|
7701
|
+
if (unlinked <= 0) continue;
|
|
7702
|
+
const note = index.notes.get(entityPath);
|
|
7703
|
+
const displayName = note?.title || name;
|
|
7704
|
+
unlinkedEntities.push({ entity: displayName, path: entityPath, unlinked_mentions: unlinked });
|
|
7705
|
+
}
|
|
7706
|
+
unlinkedEntities.sort((a, b) => b.unlinked_mentions - a.unlinked_mentions);
|
|
7707
|
+
const results = {
|
|
7708
|
+
last_sweep_at: Date.now(),
|
|
7709
|
+
sweep_duration_ms: Date.now() - start,
|
|
7710
|
+
dead_link_count: deadLinkCount,
|
|
7711
|
+
top_dead_targets: topDeadTargets,
|
|
7712
|
+
top_unlinked_entities: unlinkedEntities.slice(0, 10)
|
|
7713
|
+
};
|
|
7714
|
+
cachedResults = results;
|
|
7715
|
+
return results;
|
|
7716
|
+
}
|
|
7717
|
+
function startSweepTimer(getIndex, intervalMs) {
|
|
7718
|
+
const interval = Math.max(intervalMs ?? DEFAULT_SWEEP_INTERVAL_MS, MIN_SWEEP_INTERVAL_MS);
|
|
7719
|
+
setTimeout(() => {
|
|
7720
|
+
doSweep(getIndex);
|
|
7721
|
+
}, 5e3);
|
|
7722
|
+
sweepTimer = setInterval(() => {
|
|
7723
|
+
doSweep(getIndex);
|
|
7724
|
+
}, interval);
|
|
7725
|
+
if (sweepTimer && typeof sweepTimer === "object" && "unref" in sweepTimer) {
|
|
7726
|
+
sweepTimer.unref();
|
|
7727
|
+
}
|
|
7728
|
+
}
|
|
7729
|
+
function doSweep(getIndex) {
|
|
7730
|
+
if (sweepRunning) return;
|
|
7731
|
+
sweepRunning = true;
|
|
7732
|
+
try {
|
|
7733
|
+
const index = getIndex();
|
|
7734
|
+
if (index && index.notes && index.notes.size > 0) {
|
|
7735
|
+
runSweep(index);
|
|
7736
|
+
}
|
|
7737
|
+
} catch (err) {
|
|
7738
|
+
console.error("[Flywheel] Sweep error:", err);
|
|
7739
|
+
} finally {
|
|
7740
|
+
sweepRunning = false;
|
|
7741
|
+
}
|
|
7742
|
+
}
|
|
7743
|
+
function getSweepResults() {
|
|
7744
|
+
return cachedResults;
|
|
7745
|
+
}
|
|
7746
|
+
|
|
7480
7747
|
// src/tools/read/health.ts
|
|
7481
7748
|
var STALE_THRESHOLD_SECONDS = 300;
|
|
7482
7749
|
function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({}), getStateDb = () => null) {
|
|
@@ -7520,6 +7787,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7520
7787
|
trigger: z3.string(),
|
|
7521
7788
|
duration_ms: z3.number(),
|
|
7522
7789
|
files_changed: z3.number().nullable(),
|
|
7790
|
+
changed_paths: z3.array(z3.string()).nullable(),
|
|
7523
7791
|
steps: z3.array(z3.object({
|
|
7524
7792
|
name: z3.string(),
|
|
7525
7793
|
duration_ms: z3.number(),
|
|
@@ -7529,6 +7797,21 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7529
7797
|
skip_reason: z3.string().optional()
|
|
7530
7798
|
}))
|
|
7531
7799
|
}).optional().describe("Most recent watcher pipeline run with per-step timing"),
|
|
7800
|
+
recent_pipelines: z3.array(z3.object({
|
|
7801
|
+
timestamp: z3.number(),
|
|
7802
|
+
trigger: z3.string(),
|
|
7803
|
+
duration_ms: z3.number(),
|
|
7804
|
+
files_changed: z3.number().nullable(),
|
|
7805
|
+
changed_paths: z3.array(z3.string()).nullable(),
|
|
7806
|
+
steps: z3.array(z3.object({
|
|
7807
|
+
name: z3.string(),
|
|
7808
|
+
duration_ms: z3.number(),
|
|
7809
|
+
input: z3.record(z3.unknown()),
|
|
7810
|
+
output: z3.record(z3.unknown()),
|
|
7811
|
+
skipped: z3.boolean().optional(),
|
|
7812
|
+
skip_reason: z3.string().optional()
|
|
7813
|
+
}))
|
|
7814
|
+
})).optional().describe("Up to 5 most recent pipeline runs with steps data"),
|
|
7532
7815
|
fts5_ready: z3.boolean().describe("Whether the FTS5 keyword search index is ready"),
|
|
7533
7816
|
fts5_building: z3.boolean().describe("Whether the FTS5 keyword search index is currently building"),
|
|
7534
7817
|
embeddings_building: z3.boolean().describe("Whether semantic embeddings are currently building"),
|
|
@@ -7536,6 +7819,26 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7536
7819
|
embeddings_count: z3.coerce.number().describe("Number of notes with semantic embeddings"),
|
|
7537
7820
|
tasks_ready: z3.boolean().describe("Whether the task cache is ready to serve queries"),
|
|
7538
7821
|
tasks_building: z3.boolean().describe("Whether the task cache is currently rebuilding"),
|
|
7822
|
+
dead_link_count: z3.coerce.number().describe("Total number of broken/dead wikilinks across the vault"),
|
|
7823
|
+
top_dead_link_targets: z3.array(z3.object({
|
|
7824
|
+
target: z3.string().describe("The dead link target"),
|
|
7825
|
+
mention_count: z3.coerce.number().describe("How many notes reference this dead target")
|
|
7826
|
+
})).describe("Top 5 most-referenced dead link targets (highest-ROI candidates to create)"),
|
|
7827
|
+
sweep: z3.object({
|
|
7828
|
+
last_sweep_at: z3.number().describe("When the last background sweep completed (ms epoch)"),
|
|
7829
|
+
sweep_duration_ms: z3.number().describe("How long the last sweep took"),
|
|
7830
|
+
dead_link_count: z3.number().describe("Dead links found by sweep"),
|
|
7831
|
+
top_dead_targets: z3.array(z3.object({
|
|
7832
|
+
target: z3.string(),
|
|
7833
|
+
wikilink_references: z3.number(),
|
|
7834
|
+
content_mentions: z3.number()
|
|
7835
|
+
})).describe("Top dead link targets with FTS5 content mention counts"),
|
|
7836
|
+
top_unlinked_entities: z3.array(z3.object({
|
|
7837
|
+
entity: z3.string(),
|
|
7838
|
+
path: z3.string(),
|
|
7839
|
+
unlinked_mentions: z3.number()
|
|
7840
|
+
})).describe("Entities with the most unlinked plain-text mentions")
|
|
7841
|
+
}).optional().describe("Background sweep results (graph hygiene metrics, updated every 5 min)"),
|
|
7539
7842
|
recommendations: z3.array(z3.string()).describe("Suggested actions if any issues detected")
|
|
7540
7843
|
};
|
|
7541
7844
|
server2.registerTool(
|
|
@@ -7626,6 +7929,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7626
7929
|
}
|
|
7627
7930
|
}
|
|
7628
7931
|
let lastPipeline;
|
|
7932
|
+
let recentPipelines;
|
|
7629
7933
|
if (stateDb2) {
|
|
7630
7934
|
try {
|
|
7631
7935
|
const evt = getRecentPipelineEvent(stateDb2);
|
|
@@ -7635,13 +7939,42 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7635
7939
|
trigger: evt.trigger,
|
|
7636
7940
|
duration_ms: evt.duration_ms,
|
|
7637
7941
|
files_changed: evt.files_changed,
|
|
7942
|
+
changed_paths: evt.changed_paths,
|
|
7638
7943
|
steps: evt.steps
|
|
7639
7944
|
};
|
|
7640
7945
|
}
|
|
7641
7946
|
} catch {
|
|
7642
7947
|
}
|
|
7948
|
+
try {
|
|
7949
|
+
const events = getRecentIndexEvents(stateDb2, 10).filter((e) => e.steps && e.steps.length > 0).slice(0, 5);
|
|
7950
|
+
if (events.length > 0) {
|
|
7951
|
+
recentPipelines = events.map((e) => ({
|
|
7952
|
+
timestamp: e.timestamp,
|
|
7953
|
+
trigger: e.trigger,
|
|
7954
|
+
duration_ms: e.duration_ms,
|
|
7955
|
+
files_changed: e.files_changed,
|
|
7956
|
+
changed_paths: e.changed_paths,
|
|
7957
|
+
steps: e.steps
|
|
7958
|
+
}));
|
|
7959
|
+
}
|
|
7960
|
+
} catch {
|
|
7961
|
+
}
|
|
7643
7962
|
}
|
|
7644
7963
|
const ftsState = getFTS5State();
|
|
7964
|
+
let deadLinkCount = 0;
|
|
7965
|
+
const deadTargetCounts = /* @__PURE__ */ new Map();
|
|
7966
|
+
if (indexBuilt) {
|
|
7967
|
+
for (const note of index.notes.values()) {
|
|
7968
|
+
for (const link of note.outlinks) {
|
|
7969
|
+
if (!resolveTarget(index, link.target)) {
|
|
7970
|
+
deadLinkCount++;
|
|
7971
|
+
const key = link.target.toLowerCase();
|
|
7972
|
+
deadTargetCounts.set(key, (deadTargetCounts.get(key) || 0) + 1);
|
|
7973
|
+
}
|
|
7974
|
+
}
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
const topDeadLinkTargets = Array.from(deadTargetCounts.entries()).map(([target, mention_count]) => ({ target, mention_count })).sort((a, b) => b.mention_count - a.mention_count).slice(0, 5);
|
|
7645
7978
|
const output = {
|
|
7646
7979
|
status,
|
|
7647
7980
|
schema_version: SCHEMA_VERSION,
|
|
@@ -7661,6 +7994,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7661
7994
|
config: configInfo,
|
|
7662
7995
|
last_rebuild: lastRebuild,
|
|
7663
7996
|
last_pipeline: lastPipeline,
|
|
7997
|
+
recent_pipelines: recentPipelines,
|
|
7664
7998
|
fts5_ready: ftsState.ready,
|
|
7665
7999
|
fts5_building: ftsState.building,
|
|
7666
8000
|
embeddings_building: isEmbeddingsBuilding(),
|
|
@@ -7668,6 +8002,9 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7668
8002
|
embeddings_count: getEmbeddingsCount(),
|
|
7669
8003
|
tasks_ready: isTaskCacheReady(),
|
|
7670
8004
|
tasks_building: isTaskCacheBuilding(),
|
|
8005
|
+
dead_link_count: deadLinkCount,
|
|
8006
|
+
top_dead_link_targets: topDeadLinkTargets,
|
|
8007
|
+
sweep: getSweepResults() ?? void 0,
|
|
7671
8008
|
recommendations
|
|
7672
8009
|
};
|
|
7673
8010
|
return {
|
|
@@ -8161,6 +8498,14 @@ function generateAliasCandidates(entityName, existingAliases) {
|
|
|
8161
8498
|
candidates.push({ candidate: short, type: "short_form" });
|
|
8162
8499
|
}
|
|
8163
8500
|
}
|
|
8501
|
+
const STOPWORDS2 = /* @__PURE__ */ new Set(["the", "and", "for", "with", "from", "into", "that", "this", "are", "was", "has", "its"]);
|
|
8502
|
+
for (const word of words) {
|
|
8503
|
+
if (word.length < 4) continue;
|
|
8504
|
+
if (STOPWORDS2.has(word.toLowerCase())) continue;
|
|
8505
|
+
if (existing.has(word.toLowerCase())) continue;
|
|
8506
|
+
if (words.length >= 3 && word === words[0]) continue;
|
|
8507
|
+
candidates.push({ candidate: word, type: "name_fragment" });
|
|
8508
|
+
}
|
|
8164
8509
|
}
|
|
8165
8510
|
return candidates;
|
|
8166
8511
|
}
|
|
@@ -8187,6 +8532,7 @@ function suggestEntityAliases(stateDb2, folder) {
|
|
|
8187
8532
|
mentions = result?.cnt ?? 0;
|
|
8188
8533
|
} catch {
|
|
8189
8534
|
}
|
|
8535
|
+
if (type === "name_fragment" && mentions < 5) continue;
|
|
8190
8536
|
suggestions.push({
|
|
8191
8537
|
entity: row.name,
|
|
8192
8538
|
entity_path: row.path,
|
|
@@ -8717,6 +9063,61 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
8717
9063
|
};
|
|
8718
9064
|
}
|
|
8719
9065
|
);
|
|
9066
|
+
server2.registerTool(
|
|
9067
|
+
"unlinked_mentions_report",
|
|
9068
|
+
{
|
|
9069
|
+
title: "Unlinked Mentions Report",
|
|
9070
|
+
description: "Find which entities have the most unlinked mentions across the vault \u2014 highest-ROI linking opportunities. Uses FTS5 to count mentions and subtracts known wikilinks.",
|
|
9071
|
+
inputSchema: {
|
|
9072
|
+
limit: z5.coerce.number().default(20).describe("Maximum entities to return (default 20)")
|
|
9073
|
+
}
|
|
9074
|
+
},
|
|
9075
|
+
async ({ limit: requestedLimit }) => {
|
|
9076
|
+
requireIndex();
|
|
9077
|
+
const index = getIndex();
|
|
9078
|
+
const limit = Math.min(requestedLimit ?? 20, 100);
|
|
9079
|
+
const linkedCounts = /* @__PURE__ */ new Map();
|
|
9080
|
+
for (const note of index.notes.values()) {
|
|
9081
|
+
for (const link of note.outlinks) {
|
|
9082
|
+
const key = link.target.toLowerCase();
|
|
9083
|
+
linkedCounts.set(key, (linkedCounts.get(key) || 0) + 1);
|
|
9084
|
+
}
|
|
9085
|
+
}
|
|
9086
|
+
const results = [];
|
|
9087
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9088
|
+
for (const [name, entityPath] of index.entities) {
|
|
9089
|
+
if (seen.has(entityPath)) continue;
|
|
9090
|
+
seen.add(entityPath);
|
|
9091
|
+
const totalMentions = countFTS5Mentions(name);
|
|
9092
|
+
if (totalMentions === 0) continue;
|
|
9093
|
+
const pathKey = entityPath.toLowerCase().replace(/\.md$/, "");
|
|
9094
|
+
const linkedByName = linkedCounts.get(name) || 0;
|
|
9095
|
+
const linkedByPath = linkedCounts.get(pathKey) || 0;
|
|
9096
|
+
const linked = Math.max(linkedByName, linkedByPath);
|
|
9097
|
+
const unlinked = Math.max(0, totalMentions - linked - 1);
|
|
9098
|
+
if (unlinked <= 0) continue;
|
|
9099
|
+
const note = index.notes.get(entityPath);
|
|
9100
|
+
const displayName = note?.title || name;
|
|
9101
|
+
results.push({
|
|
9102
|
+
entity: displayName,
|
|
9103
|
+
path: entityPath,
|
|
9104
|
+
total_mentions: totalMentions,
|
|
9105
|
+
linked_mentions: linked,
|
|
9106
|
+
unlinked_mentions: unlinked
|
|
9107
|
+
});
|
|
9108
|
+
}
|
|
9109
|
+
results.sort((a, b) => b.unlinked_mentions - a.unlinked_mentions);
|
|
9110
|
+
const top = results.slice(0, limit);
|
|
9111
|
+
const output = {
|
|
9112
|
+
total_entities_checked: seen.size,
|
|
9113
|
+
entities_with_unlinked: results.length,
|
|
9114
|
+
top_entities: top
|
|
9115
|
+
};
|
|
9116
|
+
return {
|
|
9117
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
9118
|
+
};
|
|
9119
|
+
}
|
|
9120
|
+
);
|
|
8720
9121
|
}
|
|
8721
9122
|
|
|
8722
9123
|
// src/tools/read/primitives.ts
|
|
@@ -12075,8 +12476,12 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
12075
12476
|
return formatMcpResult(errorResult(notePath, `Template not found: ${template}`));
|
|
12076
12477
|
}
|
|
12077
12478
|
}
|
|
12479
|
+
const now = /* @__PURE__ */ new Date();
|
|
12078
12480
|
if (!effectiveFrontmatter.date) {
|
|
12079
|
-
effectiveFrontmatter.date =
|
|
12481
|
+
effectiveFrontmatter.date = now.toISOString().split("T")[0];
|
|
12482
|
+
}
|
|
12483
|
+
if (!effectiveFrontmatter.created) {
|
|
12484
|
+
effectiveFrontmatter.created = now.toISOString();
|
|
12080
12485
|
}
|
|
12081
12486
|
const warnings = [];
|
|
12082
12487
|
const noteName = path21.basename(notePath, ".md");
|
|
@@ -15542,6 +15947,9 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
15542
15947
|
);
|
|
15543
15948
|
}
|
|
15544
15949
|
|
|
15950
|
+
// src/index.ts
|
|
15951
|
+
import * as fs30 from "node:fs/promises";
|
|
15952
|
+
|
|
15545
15953
|
// src/resources/vault.ts
|
|
15546
15954
|
function registerVaultResources(server2, getIndex) {
|
|
15547
15955
|
server2.registerResource(
|
|
@@ -15884,6 +16292,7 @@ registerSystemTools(
|
|
|
15884
16292
|
() => vaultPath,
|
|
15885
16293
|
(newConfig) => {
|
|
15886
16294
|
flywheelConfig = newConfig;
|
|
16295
|
+
setWikilinkConfig(newConfig);
|
|
15887
16296
|
},
|
|
15888
16297
|
() => stateDb
|
|
15889
16298
|
);
|
|
@@ -15910,6 +16319,7 @@ registerConfigTools(
|
|
|
15910
16319
|
() => flywheelConfig,
|
|
15911
16320
|
(newConfig) => {
|
|
15912
16321
|
flywheelConfig = newConfig;
|
|
16322
|
+
setWikilinkConfig(newConfig);
|
|
15913
16323
|
},
|
|
15914
16324
|
() => stateDb
|
|
15915
16325
|
);
|
|
@@ -16034,31 +16444,14 @@ async function main() {
|
|
|
16034
16444
|
}
|
|
16035
16445
|
}
|
|
16036
16446
|
}
|
|
16447
|
+
var DEFAULT_ENTITY_EXCLUDE_FOLDERS = ["node_modules", "templates", "attachments", "tmp"];
|
|
16037
16448
|
async function updateEntitiesInStateDb() {
|
|
16038
16449
|
if (!stateDb) return;
|
|
16039
16450
|
try {
|
|
16451
|
+
const config = loadConfig(stateDb);
|
|
16452
|
+
const excludeFolders = config.exclude_entity_folders?.length ? config.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
|
|
16040
16453
|
const entityIndex2 = await scanVaultEntities3(vaultPath, {
|
|
16041
|
-
excludeFolders
|
|
16042
|
-
"daily-notes",
|
|
16043
|
-
"daily",
|
|
16044
|
-
"weekly",
|
|
16045
|
-
"weekly-notes",
|
|
16046
|
-
"monthly",
|
|
16047
|
-
"monthly-notes",
|
|
16048
|
-
"quarterly",
|
|
16049
|
-
"yearly-notes",
|
|
16050
|
-
"periodic",
|
|
16051
|
-
"journal",
|
|
16052
|
-
"inbox",
|
|
16053
|
-
"templates",
|
|
16054
|
-
"attachments",
|
|
16055
|
-
"tmp",
|
|
16056
|
-
"clippings",
|
|
16057
|
-
"readwise",
|
|
16058
|
-
"articles",
|
|
16059
|
-
"bookmarks",
|
|
16060
|
-
"web-clips"
|
|
16061
|
-
]
|
|
16454
|
+
excludeFolders
|
|
16062
16455
|
});
|
|
16063
16456
|
stateDb.replaceAllEntities(entityIndex2);
|
|
16064
16457
|
serverLog("index", `Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
|
|
@@ -16109,6 +16502,7 @@ async function runPostIndexWork(index) {
|
|
|
16109
16502
|
saveConfig(stateDb, inferred, existing);
|
|
16110
16503
|
}
|
|
16111
16504
|
flywheelConfig = loadConfig(stateDb);
|
|
16505
|
+
setWikilinkConfig(flywheelConfig);
|
|
16112
16506
|
const configKeys = Object.keys(flywheelConfig).filter((k) => flywheelConfig[k] != null);
|
|
16113
16507
|
serverLog("config", `Config inferred: ${configKeys.join(", ")}`);
|
|
16114
16508
|
if (stateDb) {
|
|
@@ -16177,9 +16571,22 @@ async function runPostIndexWork(index) {
|
|
|
16177
16571
|
const entityDiff = computeEntityDiff(entitiesBefore, entitiesAfter);
|
|
16178
16572
|
tracker.end({ entity_count: entitiesAfter.length, ...entityDiff });
|
|
16179
16573
|
serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
|
|
16574
|
+
const hubBefore = /* @__PURE__ */ new Map();
|
|
16575
|
+
if (stateDb) {
|
|
16576
|
+
const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
|
|
16577
|
+
for (const r of rows) hubBefore.set(r.name, r.hub_score);
|
|
16578
|
+
}
|
|
16180
16579
|
tracker.start("hub_scores", { entity_count: entitiesAfter.length });
|
|
16181
16580
|
const hubUpdated = await exportHubScores(vaultIndex, stateDb);
|
|
16182
|
-
|
|
16581
|
+
const hubDiffs = [];
|
|
16582
|
+
if (stateDb) {
|
|
16583
|
+
const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
|
|
16584
|
+
for (const r of rows) {
|
|
16585
|
+
const prev = hubBefore.get(r.name) ?? 0;
|
|
16586
|
+
if (prev !== r.hub_score) hubDiffs.push({ entity: r.name, before: prev, after: r.hub_score });
|
|
16587
|
+
}
|
|
16588
|
+
}
|
|
16589
|
+
tracker.end({ updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) });
|
|
16183
16590
|
serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
|
|
16184
16591
|
if (hasEmbeddingsIndex()) {
|
|
16185
16592
|
tracker.start("note_embeddings", { files: batch.events.length });
|
|
@@ -16206,6 +16613,7 @@ async function runPostIndexWork(index) {
|
|
|
16206
16613
|
if (hasEntityEmbeddingsIndex() && stateDb) {
|
|
16207
16614
|
tracker.start("entity_embeddings", { files: batch.events.length });
|
|
16208
16615
|
let entEmbUpdated = 0;
|
|
16616
|
+
const entEmbNames = [];
|
|
16209
16617
|
try {
|
|
16210
16618
|
const allEntities = getAllEntitiesFromDb3(stateDb);
|
|
16211
16619
|
for (const event of batch.events) {
|
|
@@ -16219,11 +16627,12 @@ async function runPostIndexWork(index) {
|
|
|
16219
16627
|
aliases: entity.aliases
|
|
16220
16628
|
}, vaultPath);
|
|
16221
16629
|
entEmbUpdated++;
|
|
16630
|
+
entEmbNames.push(entity.name);
|
|
16222
16631
|
}
|
|
16223
16632
|
}
|
|
16224
16633
|
} catch {
|
|
16225
16634
|
}
|
|
16226
|
-
tracker.end({ updated: entEmbUpdated });
|
|
16635
|
+
tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10) });
|
|
16227
16636
|
serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated`);
|
|
16228
16637
|
} else {
|
|
16229
16638
|
tracker.skip("entity_embeddings", !stateDb ? "no stateDb" : "not built");
|
|
@@ -16258,6 +16667,37 @@ async function runPostIndexWork(index) {
|
|
|
16258
16667
|
}
|
|
16259
16668
|
tracker.end({ updated: taskUpdated, removed: taskRemoved });
|
|
16260
16669
|
serverLog("watcher", `Task cache: ${taskUpdated} updated, ${taskRemoved} removed`);
|
|
16670
|
+
tracker.start("wikilink_check", { files: batch.events.length });
|
|
16671
|
+
const trackedLinks = [];
|
|
16672
|
+
if (stateDb) {
|
|
16673
|
+
for (const event of batch.events) {
|
|
16674
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
16675
|
+
try {
|
|
16676
|
+
const apps = getTrackedApplications(stateDb, event.path);
|
|
16677
|
+
if (apps.length > 0) trackedLinks.push({ file: event.path, entities: apps });
|
|
16678
|
+
} catch {
|
|
16679
|
+
}
|
|
16680
|
+
}
|
|
16681
|
+
}
|
|
16682
|
+
tracker.end({ tracked: trackedLinks });
|
|
16683
|
+
serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files`);
|
|
16684
|
+
tracker.start("implicit_feedback", { files: batch.events.length });
|
|
16685
|
+
const feedbackResults = [];
|
|
16686
|
+
if (stateDb) {
|
|
16687
|
+
for (const event of batch.events) {
|
|
16688
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
16689
|
+
try {
|
|
16690
|
+
const content = await fs30.readFile(path29.join(vaultPath, event.path), "utf-8");
|
|
16691
|
+
const removed = processImplicitFeedback(stateDb, event.path, content);
|
|
16692
|
+
for (const entity of removed) feedbackResults.push({ entity, file: event.path });
|
|
16693
|
+
} catch {
|
|
16694
|
+
}
|
|
16695
|
+
}
|
|
16696
|
+
}
|
|
16697
|
+
tracker.end({ removals: feedbackResults });
|
|
16698
|
+
if (feedbackResults.length > 0) {
|
|
16699
|
+
serverLog("watcher", `Implicit feedback: ${feedbackResults.length} removals detected`);
|
|
16700
|
+
}
|
|
16261
16701
|
const duration = Date.now() - batchStart;
|
|
16262
16702
|
if (stateDb) {
|
|
16263
16703
|
recordIndexEvent(stateDb, {
|
|
@@ -16300,6 +16740,8 @@ async function runPostIndexWork(index) {
|
|
|
16300
16740
|
watcher.start();
|
|
16301
16741
|
serverLog("watcher", "File watcher started");
|
|
16302
16742
|
}
|
|
16743
|
+
startSweepTimer(() => vaultIndex);
|
|
16744
|
+
serverLog("server", "Sweep timer started (5 min interval)");
|
|
16303
16745
|
const postDuration = Date.now() - postStart;
|
|
16304
16746
|
serverLog("server", `Post-index work complete in ${postDuration}ms`);
|
|
16305
16747
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.34",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
53
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
53
|
+
"@velvetmonkey/vault-core": "^2.0.34",
|
|
54
54
|
"better-sqlite3": "^11.0.0",
|
|
55
55
|
"chokidar": "^4.0.0",
|
|
56
56
|
"gray-matter": "^4.0.3",
|