@velvetmonkey/flywheel-memory 2.0.9 → 2.0.10
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 +125 -21
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5023,6 +5023,82 @@ function getLinkPath(index, fromPath, toPath, maxDepth = 10) {
|
|
|
5023
5023
|
}
|
|
5024
5024
|
return { exists: false, path: [], length: -1 };
|
|
5025
5025
|
}
|
|
5026
|
+
function getWeightedLinkPath(index, fromPath, toPath, maxDepth = 10) {
|
|
5027
|
+
const from = index.notes.has(fromPath) ? fromPath : resolveTarget(index, fromPath);
|
|
5028
|
+
const to = index.notes.has(toPath) ? toPath : resolveTarget(index, toPath);
|
|
5029
|
+
if (!from || !to) {
|
|
5030
|
+
return { exists: false, path: [], length: -1, total_weight: 0, weights: [] };
|
|
5031
|
+
}
|
|
5032
|
+
if (from === to) {
|
|
5033
|
+
return { exists: true, path: [from], length: 0, total_weight: 0, weights: [] };
|
|
5034
|
+
}
|
|
5035
|
+
const hubNotes = findHubNotes(index, 0);
|
|
5036
|
+
const connectionCounts = /* @__PURE__ */ new Map();
|
|
5037
|
+
for (const hub of hubNotes) {
|
|
5038
|
+
connectionCounts.set(hub.path, hub.total_connections);
|
|
5039
|
+
}
|
|
5040
|
+
const dist = /* @__PURE__ */ new Map();
|
|
5041
|
+
const prev = /* @__PURE__ */ new Map();
|
|
5042
|
+
const depthMap = /* @__PURE__ */ new Map();
|
|
5043
|
+
dist.set(from, 0);
|
|
5044
|
+
depthMap.set(from, 0);
|
|
5045
|
+
const pq = [{ node: from, cost: 0 }];
|
|
5046
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5047
|
+
while (pq.length > 0) {
|
|
5048
|
+
const { node: current, cost: currentCost } = pq.shift();
|
|
5049
|
+
if (current === to) break;
|
|
5050
|
+
if (visited.has(current)) continue;
|
|
5051
|
+
visited.add(current);
|
|
5052
|
+
const currentDepth = depthMap.get(current) ?? 0;
|
|
5053
|
+
if (currentDepth >= maxDepth) continue;
|
|
5054
|
+
const note = index.notes.get(current);
|
|
5055
|
+
if (!note) continue;
|
|
5056
|
+
for (const link of note.outlinks) {
|
|
5057
|
+
const targetPath = resolveTarget(index, link.target);
|
|
5058
|
+
if (!targetPath || visited.has(targetPath)) continue;
|
|
5059
|
+
const strength = getConnectionStrength(index, current, targetPath);
|
|
5060
|
+
const baseWeight = 1 / (1 + strength.score);
|
|
5061
|
+
const targetConnections = connectionCounts.get(targetPath) ?? 0;
|
|
5062
|
+
const hubPenalty = targetConnections > 0 ? 1 + Math.log2(targetConnections) : 1;
|
|
5063
|
+
const edgeWeight = baseWeight * hubPenalty;
|
|
5064
|
+
const newDist = currentCost + edgeWeight;
|
|
5065
|
+
const existingDist = dist.get(targetPath);
|
|
5066
|
+
if (existingDist === void 0 || newDist < existingDist) {
|
|
5067
|
+
dist.set(targetPath, newDist);
|
|
5068
|
+
prev.set(targetPath, current);
|
|
5069
|
+
depthMap.set(targetPath, currentDepth + 1);
|
|
5070
|
+
const entry = { node: targetPath, cost: newDist };
|
|
5071
|
+
const insertIdx = pq.findIndex((e) => e.cost > newDist);
|
|
5072
|
+
if (insertIdx === -1) {
|
|
5073
|
+
pq.push(entry);
|
|
5074
|
+
} else {
|
|
5075
|
+
pq.splice(insertIdx, 0, entry);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
if (!dist.has(to)) {
|
|
5081
|
+
return { exists: false, path: [], length: -1, total_weight: 0, weights: [] };
|
|
5082
|
+
}
|
|
5083
|
+
const resultPath = [];
|
|
5084
|
+
let node = to;
|
|
5085
|
+
while (node) {
|
|
5086
|
+
resultPath.unshift(node);
|
|
5087
|
+
node = prev.get(node);
|
|
5088
|
+
}
|
|
5089
|
+
const weights = [];
|
|
5090
|
+
for (let i = 0; i < resultPath.length - 1; i++) {
|
|
5091
|
+
const edgeDist = (dist.get(resultPath[i + 1]) ?? 0) - (dist.get(resultPath[i]) ?? 0);
|
|
5092
|
+
weights.push(Math.round(edgeDist * 1e3) / 1e3);
|
|
5093
|
+
}
|
|
5094
|
+
return {
|
|
5095
|
+
exists: true,
|
|
5096
|
+
path: resultPath,
|
|
5097
|
+
length: resultPath.length - 1,
|
|
5098
|
+
total_weight: Math.round((dist.get(to) ?? 0) * 1e3) / 1e3,
|
|
5099
|
+
weights
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5026
5102
|
function getCommonNeighbors(index, noteAPath, noteBPath) {
|
|
5027
5103
|
const noteA = index.notes.get(noteAPath);
|
|
5028
5104
|
const noteB = index.notes.get(noteBPath);
|
|
@@ -6103,15 +6179,20 @@ function matchesFrontmatter(note, where) {
|
|
|
6103
6179
|
}
|
|
6104
6180
|
return true;
|
|
6105
6181
|
}
|
|
6106
|
-
function hasTag(note, tag) {
|
|
6182
|
+
function hasTag(note, tag, includeChildren = false) {
|
|
6107
6183
|
const normalizedTag = tag.replace(/^#/, "").toLowerCase();
|
|
6108
|
-
return note.tags.some((t) =>
|
|
6184
|
+
return note.tags.some((t) => {
|
|
6185
|
+
const normalizedNoteTag = t.toLowerCase();
|
|
6186
|
+
if (normalizedNoteTag === normalizedTag) return true;
|
|
6187
|
+
if (includeChildren && normalizedNoteTag.startsWith(normalizedTag + "/")) return true;
|
|
6188
|
+
return false;
|
|
6189
|
+
});
|
|
6109
6190
|
}
|
|
6110
|
-
function hasAnyTag(note, tags) {
|
|
6111
|
-
return tags.some((tag) => hasTag(note, tag));
|
|
6191
|
+
function hasAnyTag(note, tags, includeChildren = false) {
|
|
6192
|
+
return tags.some((tag) => hasTag(note, tag, includeChildren));
|
|
6112
6193
|
}
|
|
6113
|
-
function hasAllTags(note, tags) {
|
|
6114
|
-
return tags.every((tag) => hasTag(note, tag));
|
|
6194
|
+
function hasAllTags(note, tags, includeChildren = false) {
|
|
6195
|
+
return tags.every((tag) => hasTag(note, tag, includeChildren));
|
|
6115
6196
|
}
|
|
6116
6197
|
function inFolder(note, folder) {
|
|
6117
6198
|
const normalizedFolder = folder.endsWith("/") ? folder : folder + "/";
|
|
@@ -6150,6 +6231,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6150
6231
|
has_tag: z4.string().optional().describe("Filter to notes with this tag"),
|
|
6151
6232
|
has_any_tag: z4.array(z4.string()).optional().describe("Filter to notes with any of these tags"),
|
|
6152
6233
|
has_all_tags: z4.array(z4.string()).optional().describe("Filter to notes with all of these tags"),
|
|
6234
|
+
include_children: z4.boolean().default(false).describe('When true, tag filters also match child tags (e.g., has_tag: "project" also matches "project/active")'),
|
|
6153
6235
|
folder: z4.string().optional().describe("Limit to notes in this folder"),
|
|
6154
6236
|
title_contains: z4.string().optional().describe("Filter to notes whose title contains this text (case-insensitive)"),
|
|
6155
6237
|
// Date filters (absorbs temporal tools)
|
|
@@ -6163,7 +6245,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6163
6245
|
// Pagination
|
|
6164
6246
|
limit: z4.number().default(20).describe("Maximum number of results to return")
|
|
6165
6247
|
},
|
|
6166
|
-
async ({ query, scope, where, has_tag, has_any_tag, has_all_tags, folder, title_contains, modified_after, modified_before, sort_by, order, prefix, limit: requestedLimit }) => {
|
|
6248
|
+
async ({ query, scope, where, has_tag, has_any_tag, has_all_tags, include_children, folder, title_contains, modified_after, modified_before, sort_by, order, prefix, limit: requestedLimit }) => {
|
|
6167
6249
|
const limit = Math.min(requestedLimit ?? 20, MAX_LIMIT);
|
|
6168
6250
|
const index = getIndex();
|
|
6169
6251
|
const vaultPath2 = getVaultPath();
|
|
@@ -6189,13 +6271,13 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6189
6271
|
matchingNotes = matchingNotes.filter((note) => matchesFrontmatter(note, where));
|
|
6190
6272
|
}
|
|
6191
6273
|
if (has_tag) {
|
|
6192
|
-
matchingNotes = matchingNotes.filter((note) => hasTag(note, has_tag));
|
|
6274
|
+
matchingNotes = matchingNotes.filter((note) => hasTag(note, has_tag, include_children));
|
|
6193
6275
|
}
|
|
6194
6276
|
if (has_any_tag && has_any_tag.length > 0) {
|
|
6195
|
-
matchingNotes = matchingNotes.filter((note) => hasAnyTag(note, has_any_tag));
|
|
6277
|
+
matchingNotes = matchingNotes.filter((note) => hasAnyTag(note, has_any_tag, include_children));
|
|
6196
6278
|
}
|
|
6197
6279
|
if (has_all_tags && has_all_tags.length > 0) {
|
|
6198
|
-
matchingNotes = matchingNotes.filter((note) => hasAllTags(note, has_all_tags));
|
|
6280
|
+
matchingNotes = matchingNotes.filter((note) => hasAllTags(note, has_all_tags, include_children));
|
|
6199
6281
|
}
|
|
6200
6282
|
if (folder) {
|
|
6201
6283
|
matchingNotes = matchingNotes.filter((note) => inFolder(note, folder));
|
|
@@ -7136,16 +7218,17 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7136
7218
|
"get_link_path",
|
|
7137
7219
|
{
|
|
7138
7220
|
title: "Get Link Path",
|
|
7139
|
-
description: "Find the shortest path of links between two notes.",
|
|
7221
|
+
description: "Find the shortest path of links between two notes. Use weighted=true to penalize hub nodes for more meaningful paths.",
|
|
7140
7222
|
inputSchema: {
|
|
7141
7223
|
from: z6.string().describe("Starting note path"),
|
|
7142
7224
|
to: z6.string().describe("Target note path"),
|
|
7143
|
-
max_depth: z6.coerce.number().default(10).describe("Maximum path length to search")
|
|
7225
|
+
max_depth: z6.coerce.number().default(10).describe("Maximum path length to search"),
|
|
7226
|
+
weighted: z6.boolean().default(false).describe("Use weighted path-finding that penalizes hub nodes for more meaningful paths")
|
|
7144
7227
|
}
|
|
7145
7228
|
},
|
|
7146
|
-
async ({ from, to, max_depth }) => {
|
|
7229
|
+
async ({ from, to, max_depth, weighted }) => {
|
|
7147
7230
|
const index = getIndex();
|
|
7148
|
-
const result = getLinkPath(index, from, to, max_depth);
|
|
7231
|
+
const result = weighted ? getWeightedLinkPath(index, from, to, max_depth) : getLinkPath(index, from, to, max_depth);
|
|
7149
7232
|
return {
|
|
7150
7233
|
content: [{ type: "text", text: JSON.stringify({
|
|
7151
7234
|
from,
|
|
@@ -9503,6 +9586,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9503
9586
|
{
|
|
9504
9587
|
path: z14.string().describe('Vault-relative path for the new note (e.g., "daily-notes/2026-01-28.md")'),
|
|
9505
9588
|
content: z14.string().default("").describe("Initial content for the note"),
|
|
9589
|
+
template: z14.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).'),
|
|
9506
9590
|
frontmatter: z14.record(z14.any()).default({}).describe("Frontmatter fields (JSON object)"),
|
|
9507
9591
|
overwrite: z14.boolean().default(false).describe("If true, overwrite existing file"),
|
|
9508
9592
|
commit: z14.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
|
|
@@ -9512,7 +9596,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9512
9596
|
agent_id: z14.string().optional().describe("Agent identifier for multi-agent scoping"),
|
|
9513
9597
|
session_id: z14.string().optional().describe("Session identifier for conversation scoping")
|
|
9514
9598
|
},
|
|
9515
|
-
async ({ path: notePath, content, frontmatter, overwrite, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions, agent_id, session_id }) => {
|
|
9599
|
+
async ({ path: notePath, content, template, frontmatter, overwrite, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions, agent_id, session_id }) => {
|
|
9516
9600
|
try {
|
|
9517
9601
|
if (!validatePath(vaultPath2, notePath)) {
|
|
9518
9602
|
return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
|
|
@@ -9524,9 +9608,29 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9524
9608
|
}
|
|
9525
9609
|
const dir = path18.dirname(fullPath);
|
|
9526
9610
|
await fs18.mkdir(dir, { recursive: true });
|
|
9611
|
+
let effectiveContent = content;
|
|
9612
|
+
let effectiveFrontmatter = frontmatter;
|
|
9613
|
+
if (template) {
|
|
9614
|
+
const templatePath = path18.join(vaultPath2, template);
|
|
9615
|
+
try {
|
|
9616
|
+
const raw = await fs18.readFile(templatePath, "utf-8");
|
|
9617
|
+
const matter8 = (await import("gray-matter")).default;
|
|
9618
|
+
const parsed = matter8(raw);
|
|
9619
|
+
const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
9620
|
+
const title = path18.basename(notePath, ".md");
|
|
9621
|
+
let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
|
|
9622
|
+
if (content) {
|
|
9623
|
+
templateContent = templateContent.trimEnd() + "\n\n" + content;
|
|
9624
|
+
}
|
|
9625
|
+
effectiveContent = templateContent;
|
|
9626
|
+
effectiveFrontmatter = { ...parsed.data || {}, ...frontmatter };
|
|
9627
|
+
} catch {
|
|
9628
|
+
return formatMcpResult(errorResult(notePath, `Template not found: ${template}`));
|
|
9629
|
+
}
|
|
9630
|
+
}
|
|
9527
9631
|
const warnings = [];
|
|
9528
9632
|
const noteName = path18.basename(notePath, ".md");
|
|
9529
|
-
const existingAliases = Array.isArray(
|
|
9633
|
+
const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
|
|
9530
9634
|
const preflight = checkPreflightSimilarity(noteName);
|
|
9531
9635
|
if (preflight.existingEntity) {
|
|
9532
9636
|
warnings.push({
|
|
@@ -9550,7 +9654,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9550
9654
|
suggestion: `This may cause ambiguous wikilink resolution`
|
|
9551
9655
|
});
|
|
9552
9656
|
}
|
|
9553
|
-
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(
|
|
9657
|
+
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(effectiveContent, skipWikilinks, notePath);
|
|
9554
9658
|
let suggestInfo;
|
|
9555
9659
|
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
9556
9660
|
const result = suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
|
|
@@ -9559,21 +9663,21 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9559
9663
|
suggestInfo = `Suggested: ${result.suggestions.join(", ")}`;
|
|
9560
9664
|
}
|
|
9561
9665
|
}
|
|
9562
|
-
let finalFrontmatter =
|
|
9666
|
+
let finalFrontmatter = effectiveFrontmatter;
|
|
9563
9667
|
if (agent_id || session_id) {
|
|
9564
|
-
finalFrontmatter = injectMutationMetadata(
|
|
9668
|
+
finalFrontmatter = injectMutationMetadata(effectiveFrontmatter, { agent_id, session_id });
|
|
9565
9669
|
}
|
|
9566
9670
|
await writeVaultFile(vaultPath2, notePath, processedContent, finalFrontmatter);
|
|
9567
9671
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Create]");
|
|
9568
9672
|
const infoLines = [wikilinkInfo, suggestInfo].filter(Boolean);
|
|
9569
9673
|
const previewLines = [
|
|
9570
|
-
`Frontmatter fields: ${Object.keys(
|
|
9674
|
+
`Frontmatter fields: ${Object.keys(effectiveFrontmatter).join(", ") || "none"}`,
|
|
9571
9675
|
`Content length: ${processedContent.length} chars`
|
|
9572
9676
|
];
|
|
9573
9677
|
if (infoLines.length > 0) {
|
|
9574
9678
|
previewLines.push(`(${infoLines.join("; ")})`);
|
|
9575
9679
|
}
|
|
9576
|
-
const hasAliases =
|
|
9680
|
+
const hasAliases = effectiveFrontmatter && "aliases" in effectiveFrontmatter;
|
|
9577
9681
|
if (!hasAliases) {
|
|
9578
9682
|
const aliasSuggestions = suggestAliases(noteName, existingAliases);
|
|
9579
9683
|
if (aliasSuggestions.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. 36 tools for search, backlinks, graph queries, and mutations.",
|
|
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.10",
|
|
54
54
|
"better-sqlite3": "^11.0.0",
|
|
55
55
|
"chokidar": "^4.0.0",
|
|
56
56
|
"gray-matter": "^4.0.3",
|