@velvetmonkey/flywheel-memory 2.0.49 → 2.0.50
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 +148 -9
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7381,7 +7381,7 @@ function refreshIfStale(vaultPath2, index, excludeTags) {
|
|
|
7381
7381
|
}
|
|
7382
7382
|
|
|
7383
7383
|
// src/index.ts
|
|
7384
|
-
import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb3, findEntityMatches as findEntityMatches2, getProtectedZones as getProtectedZones2, rangeOverlapsProtectedZone } from "@velvetmonkey/vault-core";
|
|
7384
|
+
import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb3, findEntityMatches as findEntityMatches2, getProtectedZones as getProtectedZones2, rangeOverlapsProtectedZone, detectImplicitEntities as detectImplicitEntities3 } from "@velvetmonkey/vault-core";
|
|
7385
7385
|
|
|
7386
7386
|
// src/tools/read/graph.ts
|
|
7387
7387
|
import * as fs9 from "fs";
|
|
@@ -7909,6 +7909,7 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
7909
7909
|
|
|
7910
7910
|
// src/tools/read/wikilinks.ts
|
|
7911
7911
|
import { z as z2 } from "zod";
|
|
7912
|
+
import { detectImplicitEntities as detectImplicitEntities2 } from "@velvetmonkey/vault-core";
|
|
7912
7913
|
function findEntityMatches(text, entities) {
|
|
7913
7914
|
const matches = [];
|
|
7914
7915
|
const sortedEntities = Array.from(entities.entries()).filter(([name]) => name.length >= 2).sort((a, b) => b[0].length - a[0].length);
|
|
@@ -8014,28 +8015,83 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8014
8015
|
text: z2.string().describe("The text to analyze for potential wikilinks"),
|
|
8015
8016
|
limit: z2.coerce.number().default(50).describe("Maximum number of suggestions to return"),
|
|
8016
8017
|
offset: z2.coerce.number().default(0).describe("Number of suggestions to skip (for pagination)"),
|
|
8017
|
-
detail: z2.boolean().default(false).describe("Include per-layer score breakdown for each suggestion")
|
|
8018
|
-
note_path: z2.string().optional().describe("Path of the note being analyzed (enables strictness override for daily notes)")
|
|
8018
|
+
detail: z2.boolean().default(false).describe("Include per-layer score breakdown for each suggestion")
|
|
8019
8019
|
},
|
|
8020
8020
|
outputSchema: SuggestWikilinksOutputSchema
|
|
8021
8021
|
},
|
|
8022
|
-
async ({ text, limit: requestedLimit, offset, detail
|
|
8022
|
+
async ({ text, limit: requestedLimit, offset, detail }) => {
|
|
8023
8023
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
8024
8024
|
const index = getIndex();
|
|
8025
8025
|
const allMatches = findEntityMatches(text, index.entities);
|
|
8026
8026
|
const matches = allMatches.slice(offset, offset + limit);
|
|
8027
|
+
const linkedSet = new Set(allMatches.map((m) => m.entity.toLowerCase()));
|
|
8028
|
+
const prospects = [];
|
|
8029
|
+
const prospectSeen = /* @__PURE__ */ new Set();
|
|
8030
|
+
for (const [target, links] of index.backlinks) {
|
|
8031
|
+
if (links.length < 2) continue;
|
|
8032
|
+
if (index.entities.has(target.toLowerCase())) continue;
|
|
8033
|
+
if (linkedSet.has(target.toLowerCase())) continue;
|
|
8034
|
+
const targetLower = target.toLowerCase();
|
|
8035
|
+
const textLower = text.toLowerCase();
|
|
8036
|
+
let searchPos = 0;
|
|
8037
|
+
while (searchPos < textLower.length) {
|
|
8038
|
+
const pos = textLower.indexOf(targetLower, searchPos);
|
|
8039
|
+
if (pos === -1) break;
|
|
8040
|
+
const end = pos + target.length;
|
|
8041
|
+
const before = pos > 0 ? text[pos - 1] : " ";
|
|
8042
|
+
const after = end < text.length ? text[end] : " ";
|
|
8043
|
+
if (/[\s\n\r.,;:!?()[\]{}'"<>-]/.test(before) && /[\s\n\r.,;:!?()[\]{}'"<>-]/.test(after)) {
|
|
8044
|
+
if (!prospectSeen.has(targetLower)) {
|
|
8045
|
+
prospectSeen.add(targetLower);
|
|
8046
|
+
prospects.push({
|
|
8047
|
+
entity: text.substring(pos, end),
|
|
8048
|
+
start: pos,
|
|
8049
|
+
end,
|
|
8050
|
+
source: "dead_link",
|
|
8051
|
+
confidence: links.length >= 3 ? "high" : "medium",
|
|
8052
|
+
backlink_count: links.length
|
|
8053
|
+
});
|
|
8054
|
+
}
|
|
8055
|
+
break;
|
|
8056
|
+
}
|
|
8057
|
+
searchPos = pos + 1;
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
const implicit = detectImplicitEntities2(text);
|
|
8061
|
+
for (const imp of implicit) {
|
|
8062
|
+
const impLower = imp.text.toLowerCase();
|
|
8063
|
+
if (linkedSet.has(impLower)) continue;
|
|
8064
|
+
if (prospectSeen.has(impLower)) {
|
|
8065
|
+
const existing = prospects.find((p) => p.entity.toLowerCase() === impLower);
|
|
8066
|
+
if (existing) {
|
|
8067
|
+
existing.source = "both";
|
|
8068
|
+
existing.confidence = "high";
|
|
8069
|
+
}
|
|
8070
|
+
continue;
|
|
8071
|
+
}
|
|
8072
|
+
prospectSeen.add(impLower);
|
|
8073
|
+
prospects.push({
|
|
8074
|
+
entity: imp.text,
|
|
8075
|
+
start: imp.start,
|
|
8076
|
+
end: imp.end,
|
|
8077
|
+
source: "implicit",
|
|
8078
|
+
confidence: "low"
|
|
8079
|
+
});
|
|
8080
|
+
}
|
|
8027
8081
|
const output = {
|
|
8028
8082
|
input_length: text.length,
|
|
8029
8083
|
suggestion_count: allMatches.length,
|
|
8030
8084
|
returned_count: matches.length,
|
|
8031
8085
|
suggestions: matches
|
|
8032
8086
|
};
|
|
8087
|
+
if (prospects.length > 0) {
|
|
8088
|
+
output.prospects = prospects;
|
|
8089
|
+
}
|
|
8033
8090
|
if (detail) {
|
|
8034
8091
|
const scored = await suggestRelatedLinks(text, {
|
|
8035
8092
|
detail: true,
|
|
8036
8093
|
maxSuggestions: limit,
|
|
8037
|
-
strictness: "balanced"
|
|
8038
|
-
...note_path ? { notePath: note_path } : {}
|
|
8094
|
+
strictness: "balanced"
|
|
8039
8095
|
});
|
|
8040
8096
|
if (scored.detailed) {
|
|
8041
8097
|
output.scored_suggestions = scored.detailed;
|
|
@@ -18625,14 +18681,22 @@ async function runPostIndexWork(index) {
|
|
|
18625
18681
|
}
|
|
18626
18682
|
}
|
|
18627
18683
|
}
|
|
18684
|
+
const newDeadLinks = [];
|
|
18685
|
+
for (const diff of linkDiffs) {
|
|
18686
|
+
const newDead = diff.added.filter((target) => !vaultIndex.entities.has(target.toLowerCase()));
|
|
18687
|
+
if (newDead.length > 0) {
|
|
18688
|
+
newDeadLinks.push({ file: diff.file, targets: newDead });
|
|
18689
|
+
}
|
|
18690
|
+
}
|
|
18628
18691
|
tracker.end({
|
|
18629
18692
|
total_resolved: totalResolved,
|
|
18630
18693
|
total_dead: totalDead,
|
|
18631
18694
|
links: forwardLinkResults,
|
|
18632
18695
|
link_diffs: linkDiffs,
|
|
18633
|
-
survived: survivedLinks
|
|
18696
|
+
survived: survivedLinks,
|
|
18697
|
+
new_dead_links: newDeadLinks
|
|
18634
18698
|
});
|
|
18635
|
-
serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead`);
|
|
18699
|
+
serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead${newDeadLinks.length > 0 ? `, ${newDeadLinks.reduce((s, d) => s + d.targets.length, 0)} new dead` : ""}`);
|
|
18636
18700
|
tracker.start("wikilink_check", { files: filteredEvents.length });
|
|
18637
18701
|
const trackedLinks = [];
|
|
18638
18702
|
if (stateDb) {
|
|
@@ -18696,6 +18760,7 @@ async function runPostIndexWork(index) {
|
|
|
18696
18760
|
tracker.end({ tracked: trackedLinks, mentions: mentionResults });
|
|
18697
18761
|
serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files, ${mentionResults.reduce((s, m) => s + m.entities.length, 0)} unwikified mentions`);
|
|
18698
18762
|
tracker.start("implicit_feedback", { files: filteredEvents.length });
|
|
18763
|
+
const preSuppressed = stateDb ? new Set(getAllSuppressionPenalties(stateDb).keys()) : /* @__PURE__ */ new Set();
|
|
18699
18764
|
const feedbackResults = [];
|
|
18700
18765
|
if (stateDb) {
|
|
18701
18766
|
for (const event of filteredEvents) {
|
|
@@ -18740,10 +18805,84 @@ async function runPostIndexWork(index) {
|
|
|
18740
18805
|
}
|
|
18741
18806
|
}
|
|
18742
18807
|
}
|
|
18743
|
-
|
|
18808
|
+
const newlySuppressed = [];
|
|
18809
|
+
if (stateDb) {
|
|
18810
|
+
const postSuppressed = getAllSuppressionPenalties(stateDb);
|
|
18811
|
+
for (const entity of postSuppressed.keys()) {
|
|
18812
|
+
if (!preSuppressed.has(entity)) {
|
|
18813
|
+
newlySuppressed.push(entity);
|
|
18814
|
+
}
|
|
18815
|
+
}
|
|
18816
|
+
}
|
|
18817
|
+
tracker.end({ removals: feedbackResults, additions: additionResults, newly_suppressed: newlySuppressed });
|
|
18744
18818
|
if (feedbackResults.length > 0 || additionResults.length > 0) {
|
|
18745
18819
|
serverLog("watcher", `Implicit feedback: ${feedbackResults.length} removals, ${additionResults.length} manual additions detected`);
|
|
18746
18820
|
}
|
|
18821
|
+
if (newlySuppressed.length > 0) {
|
|
18822
|
+
serverLog("watcher", `Suppression: ${newlySuppressed.length} entities newly suppressed: ${newlySuppressed.join(", ")}`);
|
|
18823
|
+
}
|
|
18824
|
+
tracker.start("prospect_scan", { files: filteredEvents.length });
|
|
18825
|
+
const prospectResults = [];
|
|
18826
|
+
for (const event of filteredEvents) {
|
|
18827
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
18828
|
+
try {
|
|
18829
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
18830
|
+
const zones = getProtectedZones2(content);
|
|
18831
|
+
const linkedSet = new Set(
|
|
18832
|
+
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).concat(forwardLinkResults.find((r) => r.file === event.path)?.dead ?? []).map((n) => n.toLowerCase())
|
|
18833
|
+
);
|
|
18834
|
+
const knownEntitySet = new Set(entitiesAfter.map((e) => e.nameLower));
|
|
18835
|
+
const implicitMatches = detectImplicitEntities3(content);
|
|
18836
|
+
const implicitNames = implicitMatches.filter((imp) => !linkedSet.has(imp.text.toLowerCase()) && !knownEntitySet.has(imp.text.toLowerCase())).map((imp) => imp.text);
|
|
18837
|
+
const deadLinkMatches = [];
|
|
18838
|
+
for (const [key, links] of vaultIndex.backlinks) {
|
|
18839
|
+
if (links.length < 2 || vaultIndex.entities.has(key) || linkedSet.has(key)) continue;
|
|
18840
|
+
const matches = findEntityMatches2(content, key, true);
|
|
18841
|
+
if (matches.some((m) => !rangeOverlapsProtectedZone(m.start, m.end, zones))) {
|
|
18842
|
+
deadLinkMatches.push(key);
|
|
18843
|
+
}
|
|
18844
|
+
}
|
|
18845
|
+
if (implicitNames.length > 0 || deadLinkMatches.length > 0) {
|
|
18846
|
+
prospectResults.push({ file: event.path, implicit: implicitNames, deadLinkMatches });
|
|
18847
|
+
}
|
|
18848
|
+
} catch {
|
|
18849
|
+
}
|
|
18850
|
+
}
|
|
18851
|
+
tracker.end({ prospects: prospectResults });
|
|
18852
|
+
if (prospectResults.length > 0) {
|
|
18853
|
+
const implicitCount = prospectResults.reduce((s, p) => s + p.implicit.length, 0);
|
|
18854
|
+
const deadCount = prospectResults.reduce((s, p) => s + p.deadLinkMatches.length, 0);
|
|
18855
|
+
serverLog("watcher", `Prospect scan: ${implicitCount} implicit entities, ${deadCount} dead link matches across ${prospectResults.length} files`);
|
|
18856
|
+
}
|
|
18857
|
+
tracker.start("suggestion_scoring", { files: filteredEvents.length });
|
|
18858
|
+
const suggestionResults = [];
|
|
18859
|
+
for (const event of filteredEvents) {
|
|
18860
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
18861
|
+
try {
|
|
18862
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
18863
|
+
const result = await suggestRelatedLinks(content, {
|
|
18864
|
+
maxSuggestions: 5,
|
|
18865
|
+
strictness: "balanced",
|
|
18866
|
+
notePath: event.path,
|
|
18867
|
+
detail: true
|
|
18868
|
+
});
|
|
18869
|
+
if (result.detailed && result.detailed.length > 0) {
|
|
18870
|
+
suggestionResults.push({
|
|
18871
|
+
file: event.path,
|
|
18872
|
+
top: result.detailed.slice(0, 5).map((s) => ({
|
|
18873
|
+
entity: s.entity,
|
|
18874
|
+
score: s.totalScore,
|
|
18875
|
+
confidence: s.confidence
|
|
18876
|
+
}))
|
|
18877
|
+
});
|
|
18878
|
+
}
|
|
18879
|
+
} catch {
|
|
18880
|
+
}
|
|
18881
|
+
}
|
|
18882
|
+
tracker.end({ scored_files: suggestionResults.length, suggestions: suggestionResults });
|
|
18883
|
+
if (suggestionResults.length > 0) {
|
|
18884
|
+
serverLog("watcher", `Suggestion scoring: ${suggestionResults.length} files scored`);
|
|
18885
|
+
}
|
|
18747
18886
|
tracker.start("tag_scan", { files: filteredEvents.length });
|
|
18748
18887
|
const tagDiffs = [];
|
|
18749
18888
|
if (stateDb) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.50",
|
|
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.50",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|