@velvetmonkey/flywheel-memory 2.0.143 → 2.0.145
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 +74 -33
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1883,12 +1883,12 @@ function computePosteriorMean(weightedCorrect, weightedFp) {
|
|
|
1883
1883
|
const beta_ = PRIOR_BETA + weightedFp;
|
|
1884
1884
|
return alpha / (alpha + beta_);
|
|
1885
1885
|
}
|
|
1886
|
-
function recordFeedback(stateDb2, entity, context, notePath, correct, confidence = 1) {
|
|
1886
|
+
function recordFeedback(stateDb2, entity, context, notePath, correct, confidence = 1, matchedTerm) {
|
|
1887
1887
|
try {
|
|
1888
|
-
console.error(`[Flywheel] recordFeedback: entity="${entity}" context="${context}" notePath="${notePath}" correct=${correct}`);
|
|
1888
|
+
console.error(`[Flywheel] recordFeedback: entity="${entity}" term="${matchedTerm ?? entity}" context="${context}" notePath="${notePath}" correct=${correct}`);
|
|
1889
1889
|
const result = stateDb2.db.prepare(
|
|
1890
|
-
"INSERT INTO wikilink_feedback (entity, context, note_path, correct, confidence) VALUES (?, ?, ?, ?, ?)"
|
|
1891
|
-
).run(entity, context, notePath, correct ? 1 : 0, confidence);
|
|
1890
|
+
"INSERT INTO wikilink_feedback (entity, context, note_path, correct, confidence, matched_term) VALUES (?, ?, ?, ?, ?, ?)"
|
|
1891
|
+
).run(entity, context, notePath, correct ? 1 : 0, confidence, matchedTerm ?? null);
|
|
1892
1892
|
console.error(`[Flywheel] recordFeedback: inserted id=${result.lastInsertRowid}`);
|
|
1893
1893
|
} catch (e) {
|
|
1894
1894
|
console.error(`[Flywheel] recordFeedback failed for entity="${entity}": ${e}`);
|
|
@@ -2173,9 +2173,10 @@ function getAllSuppressionPenalties(stateDb2, now) {
|
|
|
2173
2173
|
}
|
|
2174
2174
|
function trackWikilinkApplications(stateDb2, notePath, entities) {
|
|
2175
2175
|
const upsert = stateDb2.db.prepare(`
|
|
2176
|
-
INSERT INTO wikilink_applications (entity, note_path, applied_at, status)
|
|
2177
|
-
VALUES (?, ?, datetime('now'), 'applied')
|
|
2176
|
+
INSERT INTO wikilink_applications (entity, note_path, matched_term, applied_at, status)
|
|
2177
|
+
VALUES (?, ?, ?, datetime('now'), 'applied')
|
|
2178
2178
|
ON CONFLICT(entity, note_path) DO UPDATE SET
|
|
2179
|
+
matched_term = COALESCE(?, matched_term),
|
|
2179
2180
|
applied_at = datetime('now'),
|
|
2180
2181
|
status = 'applied'
|
|
2181
2182
|
`);
|
|
@@ -2183,10 +2184,12 @@ function trackWikilinkApplications(stateDb2, notePath, entities) {
|
|
|
2183
2184
|
`SELECT name FROM entities WHERE LOWER(name) = LOWER(?) LIMIT 1`
|
|
2184
2185
|
);
|
|
2185
2186
|
const transaction = stateDb2.db.transaction(() => {
|
|
2186
|
-
for (const
|
|
2187
|
-
const
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2187
|
+
for (const item of entities) {
|
|
2188
|
+
const entityName = typeof item === "string" ? item : item.entity;
|
|
2189
|
+
const matchedTerm = typeof item === "string" ? null : item.matchedTerm ?? null;
|
|
2190
|
+
const row = lookupCanonical.get(entityName);
|
|
2191
|
+
const canonicalName = row?.name ?? entityName;
|
|
2192
|
+
upsert.run(canonicalName, notePath, matchedTerm, matchedTerm);
|
|
2190
2193
|
}
|
|
2191
2194
|
});
|
|
2192
2195
|
transaction();
|
|
@@ -2199,7 +2202,7 @@ function getTrackedApplications(stateDb2, notePath) {
|
|
|
2199
2202
|
}
|
|
2200
2203
|
function getTrackedApplicationsWithTime(stateDb2, notePath) {
|
|
2201
2204
|
return stateDb2.db.prepare(
|
|
2202
|
-
`SELECT entity, applied_at FROM wikilink_applications WHERE note_path = ? AND status = 'applied'`
|
|
2205
|
+
`SELECT entity, applied_at, matched_term FROM wikilink_applications WHERE note_path = ? AND status = 'applied'`
|
|
2203
2206
|
).all(notePath);
|
|
2204
2207
|
}
|
|
2205
2208
|
function computeImplicitRemovalConfidence(appliedAt) {
|
|
@@ -2270,10 +2273,10 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
|
|
|
2270
2273
|
`UPDATE wikilink_applications SET status = 'removed' WHERE entity = ? AND note_path = ?`
|
|
2271
2274
|
);
|
|
2272
2275
|
const transaction = stateDb2.db.transaction(() => {
|
|
2273
|
-
for (const { entity, applied_at } of trackedWithTime) {
|
|
2276
|
+
for (const { entity, applied_at, matched_term } of trackedWithTime) {
|
|
2274
2277
|
if (!currentLinks.has(entity.toLowerCase())) {
|
|
2275
2278
|
const confidence = computeImplicitRemovalConfidence(applied_at);
|
|
2276
|
-
recordFeedback(stateDb2, entity, "implicit:removed", notePath, false, confidence);
|
|
2279
|
+
recordFeedback(stateDb2, entity, "implicit:removed", notePath, false, confidence, matched_term ?? void 0);
|
|
2277
2280
|
markRemoved.run(entity, notePath);
|
|
2278
2281
|
removed.push(entity);
|
|
2279
2282
|
}
|
|
@@ -2288,12 +2291,12 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
|
|
|
2288
2291
|
`SELECT MAX(created_at) as last FROM wikilink_feedback
|
|
2289
2292
|
WHERE entity = ? COLLATE NOCASE AND context = 'implicit:survived' AND note_path = ?`
|
|
2290
2293
|
);
|
|
2291
|
-
for (const { entity } of trackedWithTime) {
|
|
2294
|
+
for (const { entity, matched_term } of trackedWithTime) {
|
|
2292
2295
|
if (currentLinks.has(entity.toLowerCase())) {
|
|
2293
2296
|
const lastSurvival = getLastSurvival.get(entity, notePath);
|
|
2294
2297
|
const lastAt = lastSurvival?.last ? new Date(lastSurvival.last).getTime() : 0;
|
|
2295
2298
|
if (Date.now() - lastAt > SURVIVAL_COOLDOWN_MS) {
|
|
2296
|
-
recordFeedback(stateDb2, entity, "implicit:survived", notePath, true, 0.8);
|
|
2299
|
+
recordFeedback(stateDb2, entity, "implicit:survived", notePath, true, 0.8, matched_term ?? void 0);
|
|
2297
2300
|
}
|
|
2298
2301
|
}
|
|
2299
2302
|
}
|
|
@@ -3688,17 +3691,25 @@ function isLikelyArticleTitle(name) {
|
|
|
3688
3691
|
}
|
|
3689
3692
|
function getCrossFolderBoost(entityPath, notePath) {
|
|
3690
3693
|
if (!entityPath || !notePath) return 0;
|
|
3691
|
-
const
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
|
|
3694
|
+
const entityParts = entityPath.split("/");
|
|
3695
|
+
const noteParts = notePath.split("/");
|
|
3696
|
+
const entityFolder = entityParts[0];
|
|
3697
|
+
const noteFolder = noteParts[0];
|
|
3698
|
+
if (!entityFolder || !noteFolder) return 0;
|
|
3699
|
+
if (entityFolder === noteFolder) {
|
|
3700
|
+
if (entityParts[1] && noteParts[1] && entityParts[1] === noteParts[1]) {
|
|
3701
|
+
return 3;
|
|
3702
|
+
}
|
|
3703
|
+
return 2;
|
|
3695
3704
|
}
|
|
3696
|
-
return 0;
|
|
3705
|
+
if (GENERIC_FOLDERS.has(entityFolder)) return 0;
|
|
3706
|
+
if (GENERIC_FOLDERS.has(noteFolder)) return -1;
|
|
3707
|
+
return -3;
|
|
3697
3708
|
}
|
|
3698
3709
|
function getHubBoost(entity) {
|
|
3699
3710
|
const hubScore = entity.hubScore ?? 0;
|
|
3700
3711
|
if (hubScore <= 0) return 0;
|
|
3701
|
-
return Math.min(Math.round(Math.log2(hubScore) *
|
|
3712
|
+
return Math.min(Math.round(Math.log2(hubScore) * 6) / 10, 4);
|
|
3702
3713
|
}
|
|
3703
3714
|
function getNoteContext(notePath) {
|
|
3704
3715
|
const lower = notePath.toLowerCase();
|
|
@@ -3955,7 +3966,7 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
3955
3966
|
const recencyBoostVal = hasContentOverlap && !disabled.has("recency") ? recencyIndex ? getRecencyBoost(entityName, recencyIndex) : 0 : 0;
|
|
3956
3967
|
const rawCrossFolderBoost = disabled.has("cross_folder") ? 0 : notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
3957
3968
|
const rawHubBoost = disabled.has("hub_boost") ? 0 : getHubBoost(entity);
|
|
3958
|
-
const hubBoost = hasContentOverlap ? rawHubBoost : Math.min(rawHubBoost,
|
|
3969
|
+
const hubBoost = hasContentOverlap ? rawHubBoost : Math.min(rawHubBoost, 2);
|
|
3959
3970
|
const crossFolderBoost = hasContentOverlap ? rawCrossFolderBoost : Math.min(rawCrossFolderBoost, 2);
|
|
3960
3971
|
const feedbackAdj = disabled.has("feedback") ? 0 : feedbackBoosts.get(entityName) ?? 0;
|
|
3961
3972
|
const edgeWeightBoost = disabled.has("edge_weight") ? 0 : getEdgeWeightBoostScore(entityName, edgeWeightMap);
|
|
@@ -4112,11 +4123,37 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
4112
4123
|
return emptyResult;
|
|
4113
4124
|
}
|
|
4114
4125
|
const MAX_SUFFIX_ENTRIES = 3;
|
|
4126
|
+
const MAX_SUFFIX_PER_CATEGORY = 2;
|
|
4127
|
+
const MAX_SUFFIX_PER_FOLDER = 2;
|
|
4128
|
+
const MAX_SUFFIX_APPEARANCES = 5;
|
|
4115
4129
|
const MIN_SUFFIX_SCORE = noteContext === "daily" ? 8 : 12;
|
|
4116
4130
|
const MIN_SUFFIX_CONTENT = noteContext === "daily" ? 2 : 3;
|
|
4117
|
-
const
|
|
4118
|
-
(e
|
|
4119
|
-
|
|
4131
|
+
const suffixCandidates = topEntries.filter((e) => {
|
|
4132
|
+
if (e.score < MIN_SUFFIX_SCORE) return false;
|
|
4133
|
+
if (!(e.breakdown.contentMatch >= MIN_SUFFIX_CONTENT || e.breakdown.cooccurrenceBoost >= MIN_SUFFIX_CONTENT || (e.breakdown.semanticBoost ?? 0) >= MIN_SUFFIX_CONTENT)) return false;
|
|
4134
|
+
if (!disabled.has("fatigue")) {
|
|
4135
|
+
const escapedName = e.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4136
|
+
const suffixPattern = new RegExp(`\u2192 .*\\[\\[${escapedName}\\]\\]`, "g");
|
|
4137
|
+
const appearances = (content.match(suffixPattern) || []).length;
|
|
4138
|
+
if (appearances >= MAX_SUFFIX_APPEARANCES) return false;
|
|
4139
|
+
}
|
|
4140
|
+
return true;
|
|
4141
|
+
});
|
|
4142
|
+
const suffixEntries = [];
|
|
4143
|
+
const categoryCount = /* @__PURE__ */ new Map();
|
|
4144
|
+
const folderCount = /* @__PURE__ */ new Map();
|
|
4145
|
+
for (const c of suffixCandidates) {
|
|
4146
|
+
const cat = c.category ?? "other";
|
|
4147
|
+
const folder = c.path?.split("/")[0] ?? "";
|
|
4148
|
+
const catN = categoryCount.get(cat) ?? 0;
|
|
4149
|
+
const folderN = folderCount.get(folder) ?? 0;
|
|
4150
|
+
if (catN >= MAX_SUFFIX_PER_CATEGORY) continue;
|
|
4151
|
+
if (folderN >= MAX_SUFFIX_PER_FOLDER) continue;
|
|
4152
|
+
suffixEntries.push(c);
|
|
4153
|
+
categoryCount.set(cat, catN + 1);
|
|
4154
|
+
folderCount.set(folder, folderN + 1);
|
|
4155
|
+
if (suffixEntries.length >= MAX_SUFFIX_ENTRIES) break;
|
|
4156
|
+
}
|
|
4120
4157
|
const suffix = suffixEntries.length > 0 ? "\u2192 " + suffixEntries.map((e) => `[[${e.name}]]`).join(", ") : "";
|
|
4121
4158
|
const result = {
|
|
4122
4159
|
suggestions: topSuggestions,
|
|
@@ -4357,7 +4394,7 @@ async function applyProactiveSuggestions(filePath, vaultPath2, suggestions, conf
|
|
|
4357
4394
|
skipped: candidates.map((c) => c.entity).filter((e) => !result.linkedEntities.includes(e))
|
|
4358
4395
|
};
|
|
4359
4396
|
}
|
|
4360
|
-
var moduleStateDb4, moduleConfig, ALL_IMPLICIT_PATTERNS, entityIndex, indexReady, indexError2, lastLoadedAt, cooccurrenceIndex, recencyIndex, DEFAULT_EXCLUDE_FOLDERS, SUGGESTION_PATTERN, GENERIC_WORDS, MAX_ENTITY_LENGTH, MAX_ENTITY_WORDS, ARTICLE_PATTERNS, STRICTNESS_CONFIGS, TYPE_BOOST,
|
|
4397
|
+
var moduleStateDb4, moduleConfig, ALL_IMPLICIT_PATTERNS, entityIndex, indexReady, indexError2, lastLoadedAt, cooccurrenceIndex, recencyIndex, DEFAULT_EXCLUDE_FOLDERS, SUGGESTION_PATTERN, GENERIC_WORDS, MAX_ENTITY_LENGTH, MAX_ENTITY_WORDS, ARTICLE_PATTERNS, STRICTNESS_CONFIGS, TYPE_BOOST, GENERIC_FOLDERS, SEMANTIC_MIN_SIMILARITY, SEMANTIC_MAX_BOOST, CONTEXT_BOOST, MIN_SUGGESTION_SCORE, MIN_MATCH_RATIO, FULL_ALIAS_MATCH_BONUS;
|
|
4361
4398
|
var init_wikilinks = __esm({
|
|
4362
4399
|
"src/core/write/wikilinks.ts"() {
|
|
4363
4400
|
"use strict";
|
|
@@ -4547,7 +4584,7 @@ var init_wikilinks = __esm({
|
|
|
4547
4584
|
other: 0
|
|
4548
4585
|
// Unknown category
|
|
4549
4586
|
};
|
|
4550
|
-
|
|
4587
|
+
GENERIC_FOLDERS = /* @__PURE__ */ new Set(["daily-notes", "weekly-notes", "clippings", "templates", "new"]);
|
|
4551
4588
|
SEMANTIC_MIN_SIMILARITY = 0.3;
|
|
4552
4589
|
SEMANTIC_MAX_BOOST = 12;
|
|
4553
4590
|
CONTEXT_BOOST = {
|
|
@@ -10868,9 +10905,9 @@ This server manages multiple vaults. Every tool has an optional "vault" paramete
|
|
|
10868
10905
|
}
|
|
10869
10906
|
parts.push(`
|
|
10870
10907
|
**Frontmatter matters more than content** for Flywheel's intelligence. When creating or updating notes, always set:
|
|
10871
|
-
- \`type:\` \u2014 drives entity categorization (person, project, technology). Without it, the category is guessed from the name alone.
|
|
10872
|
-
- \`aliases:\` \u2014 alternative names so the entity is found when referred to differently.
|
|
10873
|
-
- \`description:\` \u2014 one-line summary shown in search results and used by recall.
|
|
10908
|
+
- \`type:\` \u2014 drives entity categorization (person, project, technology). Without it, the category is guessed from the name alone and is often wrong.
|
|
10909
|
+
- \`aliases:\` \u2014 alternative names so the entity is found when referred to differently. Without it, the entity is invisible to searches using alternate names.
|
|
10910
|
+
- \`description:\` \u2014 one-line summary shown in search results and used by recall. Without it, search results and recall are degraded.
|
|
10874
10911
|
- Tags \u2014 used for filtering, suggestion scoring, and schema analysis.
|
|
10875
10912
|
Good frontmatter is the highest-leverage action for improving suggestions, recall, and link quality.`);
|
|
10876
10913
|
if (categories.has("read")) {
|
|
@@ -10889,6 +10926,10 @@ Escalation: "search" (enriched metadata + content preview) \u2192 "get_note_stru
|
|
|
10889
10926
|
created with the correct structure and frontmatter for this vault. Use a matching policy instead of
|
|
10890
10927
|
raw write tools when one exists. Fall back to direct tools only when no policy fits.
|
|
10891
10928
|
|
|
10929
|
+
**Every new note should have \`type\`, \`aliases\`, and \`description\` in frontmatter** \u2014 this is what powers
|
|
10930
|
+
entity categorization, search ranking, and link suggestions. Notes without frontmatter are nearly invisible
|
|
10931
|
+
to the intelligence layer.
|
|
10932
|
+
|
|
10892
10933
|
Write to existing notes with "vault_add_to_section". Create new notes with "vault_create_note".
|
|
10893
10934
|
Update metadata with "vault_update_frontmatter". These are fallback tools \u2014 use them when no policy fits.
|
|
10894
10935
|
All writes auto-link entities \u2014 no manual [[wikilinks]] needed.
|
|
@@ -18363,7 +18404,7 @@ import { z as z15 } from "zod";
|
|
|
18363
18404
|
function registerFrontmatterTools(server2, getVaultPath) {
|
|
18364
18405
|
server2.tool(
|
|
18365
18406
|
"vault_update_frontmatter",
|
|
18366
|
-
`Update frontmatter fields in a note (merge with existing). Set only_if_missing=true to only add fields that don't already exist (absorbed vault_add_frontmatter_field).
|
|
18407
|
+
`Update frontmatter fields in a note (merge with existing). Use this to backfill type, aliases, and description on notes missing them \u2014 these fields are critical for search and link quality. Set only_if_missing=true to only add fields that don't already exist (absorbed vault_add_frontmatter_field).
|
|
18367
18408
|
|
|
18368
18409
|
Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { status: "active", priority: 1 }, only_if_missing: true })`,
|
|
18369
18410
|
{
|
|
@@ -18430,12 +18471,12 @@ import path26 from "path";
|
|
|
18430
18471
|
function registerNoteTools(server2, getVaultPath, getIndex) {
|
|
18431
18472
|
server2.tool(
|
|
18432
18473
|
"vault_create_note",
|
|
18433
|
-
'Create a new note in the vault
|
|
18474
|
+
'Create a new note in the vault. Always include frontmatter with at least type, aliases, and description \u2014 these drive entity categorization, search, and link suggestions.\n\nExample: vault_create_note({ path: "people/Jane Smith.md", content: "# Jane Smith\\n\\nProduct manager at Acme.", frontmatter: { type: "person", aliases: ["Jane"], description: "Product manager at Acme", company: "Acme" } })',
|
|
18434
18475
|
{
|
|
18435
18476
|
path: z16.string().describe('Vault-relative path for the new note (e.g., "daily-notes/2026-01-28.md")'),
|
|
18436
18477
|
content: z16.string().default("").describe("Initial content for the note"),
|
|
18437
18478
|
template: z16.string().optional().describe('Vault-relative path to a template file (e.g., "templates/person.md"). Template variables {{date}} and {{title}} are substituted. Template frontmatter is merged with the frontmatter parameter (explicit values take precedence).'),
|
|
18438
|
-
frontmatter: z16.record(z16.any()).default({}).describe("Frontmatter fields (JSON object)"),
|
|
18479
|
+
frontmatter: z16.record(z16.any()).default({}).describe("Frontmatter fields (JSON object). At minimum set type, aliases, and description \u2014 without these the note is nearly invisible to search and suggestions."),
|
|
18439
18480
|
overwrite: z16.boolean().default(false).describe("If true, overwrite existing file"),
|
|
18440
18481
|
commit: z16.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
|
|
18441
18482
|
skipWikilinks: z16.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.145",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 74 tools for search, backlinks, graph queries, mutations, agent memory, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@huggingface/transformers": "^3.8.1",
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
57
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
57
|
+
"@velvetmonkey/vault-core": "^2.0.145",
|
|
58
58
|
"better-sqlite3": "^12.0.0",
|
|
59
59
|
"chokidar": "^4.0.0",
|
|
60
60
|
"gray-matter": "^4.0.3",
|