@velvetmonkey/flywheel-memory 2.0.8 → 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 +243 -30
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -567,6 +567,7 @@ function injectMutationMetadata(frontmatter, scoping) {
|
|
|
567
567
|
if (scoping.session_id) {
|
|
568
568
|
frontmatter._session_id = scoping.session_id;
|
|
569
569
|
}
|
|
570
|
+
frontmatter._source = "ai";
|
|
570
571
|
if (scoping.agent_id && scoping.session_id) {
|
|
571
572
|
frontmatter._last_modified_by = `${scoping.agent_id}:${scoping.session_id}`;
|
|
572
573
|
} else if (scoping.agent_id) {
|
|
@@ -5022,6 +5023,82 @@ function getLinkPath(index, fromPath, toPath, maxDepth = 10) {
|
|
|
5022
5023
|
}
|
|
5023
5024
|
return { exists: false, path: [], length: -1 };
|
|
5024
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
|
+
}
|
|
5025
5102
|
function getCommonNeighbors(index, noteAPath, noteBPath) {
|
|
5026
5103
|
const noteA = index.notes.get(noteAPath);
|
|
5027
5104
|
const noteB = index.notes.get(noteBPath);
|
|
@@ -6102,15 +6179,20 @@ function matchesFrontmatter(note, where) {
|
|
|
6102
6179
|
}
|
|
6103
6180
|
return true;
|
|
6104
6181
|
}
|
|
6105
|
-
function hasTag(note, tag) {
|
|
6182
|
+
function hasTag(note, tag, includeChildren = false) {
|
|
6106
6183
|
const normalizedTag = tag.replace(/^#/, "").toLowerCase();
|
|
6107
|
-
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
|
+
});
|
|
6108
6190
|
}
|
|
6109
|
-
function hasAnyTag(note, tags) {
|
|
6110
|
-
return tags.some((tag) => hasTag(note, tag));
|
|
6191
|
+
function hasAnyTag(note, tags, includeChildren = false) {
|
|
6192
|
+
return tags.some((tag) => hasTag(note, tag, includeChildren));
|
|
6111
6193
|
}
|
|
6112
|
-
function hasAllTags(note, tags) {
|
|
6113
|
-
return tags.every((tag) => hasTag(note, tag));
|
|
6194
|
+
function hasAllTags(note, tags, includeChildren = false) {
|
|
6195
|
+
return tags.every((tag) => hasTag(note, tag, includeChildren));
|
|
6114
6196
|
}
|
|
6115
6197
|
function inFolder(note, folder) {
|
|
6116
6198
|
const normalizedFolder = folder.endsWith("/") ? folder : folder + "/";
|
|
@@ -6140,7 +6222,7 @@ function sortNotes(notes, sortBy, order) {
|
|
|
6140
6222
|
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
6141
6223
|
server2.tool(
|
|
6142
6224
|
"search",
|
|
6143
|
-
'Search the vault across metadata, content, and entities. Scope controls what to search: "metadata" for frontmatter/tags/folders, "content" for full-text search (FTS5), "entities" for people/projects/technologies, "all" (default) tries metadata then falls back to content search
|
|
6225
|
+
'Search the vault across metadata, content, and entities. Scope controls what to search: "metadata" for frontmatter/tags/folders, "content" for full-text search (FTS5), "entities" for people/projects/technologies, "all" (default) tries metadata then falls back to content search.\n\nExample: search({ query: "quarterly review", scope: "content", limit: 5 })\nExample: search({ where: { type: "project", status: "active" }, scope: "metadata" })',
|
|
6144
6226
|
{
|
|
6145
6227
|
query: z4.string().optional().describe('Search query text. Required for scope "content", "entities", "all". For "metadata" scope, use filters instead.'),
|
|
6146
6228
|
scope: z4.enum(["metadata", "content", "entities", "all"]).default("all").describe("What to search: metadata (frontmatter/tags/folders), content (FTS5 full-text), entities (people/projects), all (metadata then content)"),
|
|
@@ -6149,6 +6231,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6149
6231
|
has_tag: z4.string().optional().describe("Filter to notes with this tag"),
|
|
6150
6232
|
has_any_tag: z4.array(z4.string()).optional().describe("Filter to notes with any of these tags"),
|
|
6151
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")'),
|
|
6152
6235
|
folder: z4.string().optional().describe("Limit to notes in this folder"),
|
|
6153
6236
|
title_contains: z4.string().optional().describe("Filter to notes whose title contains this text (case-insensitive)"),
|
|
6154
6237
|
// Date filters (absorbs temporal tools)
|
|
@@ -6162,7 +6245,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6162
6245
|
// Pagination
|
|
6163
6246
|
limit: z4.number().default(20).describe("Maximum number of results to return")
|
|
6164
6247
|
},
|
|
6165
|
-
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 }) => {
|
|
6166
6249
|
const limit = Math.min(requestedLimit ?? 20, MAX_LIMIT);
|
|
6167
6250
|
const index = getIndex();
|
|
6168
6251
|
const vaultPath2 = getVaultPath();
|
|
@@ -6188,13 +6271,13 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6188
6271
|
matchingNotes = matchingNotes.filter((note) => matchesFrontmatter(note, where));
|
|
6189
6272
|
}
|
|
6190
6273
|
if (has_tag) {
|
|
6191
|
-
matchingNotes = matchingNotes.filter((note) => hasTag(note, has_tag));
|
|
6274
|
+
matchingNotes = matchingNotes.filter((note) => hasTag(note, has_tag, include_children));
|
|
6192
6275
|
}
|
|
6193
6276
|
if (has_any_tag && has_any_tag.length > 0) {
|
|
6194
|
-
matchingNotes = matchingNotes.filter((note) => hasAnyTag(note, has_any_tag));
|
|
6277
|
+
matchingNotes = matchingNotes.filter((note) => hasAnyTag(note, has_any_tag, include_children));
|
|
6195
6278
|
}
|
|
6196
6279
|
if (has_all_tags && has_all_tags.length > 0) {
|
|
6197
|
-
matchingNotes = matchingNotes.filter((note) => hasAllTags(note, has_all_tags));
|
|
6280
|
+
matchingNotes = matchingNotes.filter((note) => hasAllTags(note, has_all_tags, include_children));
|
|
6198
6281
|
}
|
|
6199
6282
|
if (folder) {
|
|
6200
6283
|
matchingNotes = matchingNotes.filter((note) => inFolder(note, folder));
|
|
@@ -7135,16 +7218,17 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7135
7218
|
"get_link_path",
|
|
7136
7219
|
{
|
|
7137
7220
|
title: "Get Link Path",
|
|
7138
|
-
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.",
|
|
7139
7222
|
inputSchema: {
|
|
7140
7223
|
from: z6.string().describe("Starting note path"),
|
|
7141
7224
|
to: z6.string().describe("Target note path"),
|
|
7142
|
-
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")
|
|
7143
7227
|
}
|
|
7144
7228
|
},
|
|
7145
|
-
async ({ from, to, max_depth }) => {
|
|
7229
|
+
async ({ from, to, max_depth, weighted }) => {
|
|
7146
7230
|
const index = getIndex();
|
|
7147
|
-
const result = getLinkPath(index, from, to, max_depth);
|
|
7231
|
+
const result = weighted ? getWeightedLinkPath(index, from, to, max_depth) : getLinkPath(index, from, to, max_depth);
|
|
7148
7232
|
return {
|
|
7149
7233
|
content: [{ type: "text", text: JSON.stringify({
|
|
7150
7234
|
from,
|
|
@@ -7411,7 +7495,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath) {
|
|
|
7411
7495
|
"graph_analysis",
|
|
7412
7496
|
{
|
|
7413
7497
|
title: "Graph Analysis",
|
|
7414
|
-
description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (disconnected content)\n- "dead_ends": Notes with backlinks but no outgoing links\n- "sources": Notes with outgoing links but no backlinks\n- "hubs": Highly connected notes (many links to/from)\n- "stale": Important notes (by backlink count) not recently modified',
|
|
7498
|
+
description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (disconnected content)\n- "dead_ends": Notes with backlinks but no outgoing links\n- "sources": Notes with outgoing links but no backlinks\n- "hubs": Highly connected notes (many links to/from)\n- "stale": Important notes (by backlink count) not recently modified\n\nExample: graph_analysis({ analysis: "hubs", limit: 10 })\nExample: graph_analysis({ analysis: "stale", days: 30, min_backlinks: 3 })',
|
|
7415
7499
|
inputSchema: {
|
|
7416
7500
|
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale"]).describe("Type of graph analysis to perform"),
|
|
7417
7501
|
folder: z8.string().optional().describe("Limit to notes in this folder (orphans, dead_ends, sources)"),
|
|
@@ -7984,7 +8068,7 @@ function registerVaultSchemaTools(server2, getIndex, getVaultPath) {
|
|
|
7984
8068
|
"vault_schema",
|
|
7985
8069
|
{
|
|
7986
8070
|
title: "Vault Schema",
|
|
7987
|
-
description: 'Analyze and validate vault frontmatter schema. Use analysis to pick the mode:\n- "overview": Schema of all frontmatter fields across the vault\n- "field_values": All unique values for a specific field\n- "inconsistencies": Fields with multiple types across notes\n- "validate": Validate notes against a provided schema\n- "missing": Find notes missing expected fields by folder\n- "conventions": Auto-detect metadata conventions for a folder\n- "incomplete": Find notes missing expected fields (inferred)\n- "suggest_values": Suggest values for a field based on usage',
|
|
8071
|
+
description: 'Analyze and validate vault frontmatter schema. Use analysis to pick the mode:\n- "overview": Schema of all frontmatter fields across the vault\n- "field_values": All unique values for a specific field\n- "inconsistencies": Fields with multiple types across notes\n- "validate": Validate notes against a provided schema\n- "missing": Find notes missing expected fields by folder\n- "conventions": Auto-detect metadata conventions for a folder\n- "incomplete": Find notes missing expected fields (inferred)\n- "suggest_values": Suggest values for a field based on usage\n\nExample: vault_schema({ analysis: "field_values", field: "status" })\nExample: vault_schema({ analysis: "conventions", folder: "projects" })',
|
|
7988
8072
|
inputSchema: {
|
|
7989
8073
|
analysis: z9.enum([
|
|
7990
8074
|
"overview",
|
|
@@ -8554,7 +8638,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
|
|
|
8554
8638
|
"note_intelligence",
|
|
8555
8639
|
{
|
|
8556
8640
|
title: "Note Intelligence",
|
|
8557
|
-
description: 'Analyze a note for patterns, suggestions, and consistency. Use analysis to pick the mode:\n- "prose_patterns": Find "Key: Value" or "Key: [[wikilink]]" patterns in prose\n- "suggest_frontmatter": Suggest YAML frontmatter from detected prose patterns\n- "suggest_wikilinks": Find frontmatter values that could be wikilinks\n- "cross_layer": Check consistency between frontmatter and prose references\n- "compute": Auto-compute derived fields (word_count, link_count, etc.)\n- "all": Run all analyses and return combined result',
|
|
8641
|
+
description: 'Analyze a note for patterns, suggestions, and consistency. Use analysis to pick the mode:\n- "prose_patterns": Find "Key: Value" or "Key: [[wikilink]]" patterns in prose\n- "suggest_frontmatter": Suggest YAML frontmatter from detected prose patterns\n- "suggest_wikilinks": Find frontmatter values that could be wikilinks\n- "cross_layer": Check consistency between frontmatter and prose references\n- "compute": Auto-compute derived fields (word_count, link_count, etc.)\n- "all": Run all analyses and return combined result\n\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "wikilinks" })\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "compute", fields: ["word_count", "link_count"] })',
|
|
8558
8642
|
inputSchema: {
|
|
8559
8643
|
analysis: z10.enum([
|
|
8560
8644
|
"prose_patterns",
|
|
@@ -9031,7 +9115,9 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
9031
9115
|
function registerMutationTools(server2, vaultPath2, getConfig = () => ({})) {
|
|
9032
9116
|
server2.tool(
|
|
9033
9117
|
"vault_add_to_section",
|
|
9034
|
-
|
|
9118
|
+
`Add content to a specific section in a markdown note. Set create_if_missing=true to auto-create the note from template if it doesn't exist (enables 1-call daily capture).
|
|
9119
|
+
|
|
9120
|
+
Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", content: "Met with team about Q1", format: "timestamp-bullet", create_if_missing: true })`,
|
|
9035
9121
|
{
|
|
9036
9122
|
path: z11.string().describe('Vault-relative path to the note (e.g., "daily-notes/2026-01-28.md")'),
|
|
9037
9123
|
section: z11.string().describe('Heading text to add to (e.g., "Log" or "## Log")'),
|
|
@@ -9295,7 +9381,7 @@ function toggleTask(content, lineNumber) {
|
|
|
9295
9381
|
function registerTaskTools(server2, vaultPath2) {
|
|
9296
9382
|
server2.tool(
|
|
9297
9383
|
"vault_toggle_task",
|
|
9298
|
-
|
|
9384
|
+
'Toggle a task checkbox between checked and unchecked.\n\nExample: vault_toggle_task({ path: "daily/2026-02-15.md", task: "review PR", section: "Tasks" })',
|
|
9299
9385
|
{
|
|
9300
9386
|
path: z12.string().describe("Vault-relative path to the note"),
|
|
9301
9387
|
task: z12.string().describe("Task text to find (partial match supported)"),
|
|
@@ -9355,7 +9441,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
9355
9441
|
);
|
|
9356
9442
|
server2.tool(
|
|
9357
9443
|
"vault_add_task",
|
|
9358
|
-
|
|
9444
|
+
'Add a new task to a section in a markdown note.\n\nExample: vault_add_task({ path: "daily/2026-02-15.md", section: "Tasks", task: "Write unit tests for auth module" })',
|
|
9359
9445
|
{
|
|
9360
9446
|
path: z12.string().describe("Vault-relative path to the note"),
|
|
9361
9447
|
section: z12.string().describe("Section to add the task to"),
|
|
@@ -9433,7 +9519,9 @@ import { z as z13 } from "zod";
|
|
|
9433
9519
|
function registerFrontmatterTools(server2, vaultPath2) {
|
|
9434
9520
|
server2.tool(
|
|
9435
9521
|
"vault_update_frontmatter",
|
|
9436
|
-
|
|
9522
|
+
`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).
|
|
9523
|
+
|
|
9524
|
+
Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { status: "active", priority: 1 }, only_if_missing: true })`,
|
|
9437
9525
|
{
|
|
9438
9526
|
path: z13.string().describe("Vault-relative path to the note"),
|
|
9439
9527
|
frontmatter: z13.record(z13.any()).describe("Frontmatter fields to update (JSON object)"),
|
|
@@ -9494,10 +9582,11 @@ import path18 from "path";
|
|
|
9494
9582
|
function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
9495
9583
|
server2.tool(
|
|
9496
9584
|
"vault_create_note",
|
|
9497
|
-
|
|
9585
|
+
'Create a new note in the vault with optional frontmatter and content.\n\nExample: vault_create_note({ path: "people/Jane Smith.md", content: "# Jane Smith\\n\\nProduct manager at Acme.", frontmatter: { type: "person", company: "Acme" } })',
|
|
9498
9586
|
{
|
|
9499
9587
|
path: z14.string().describe('Vault-relative path for the new note (e.g., "daily-notes/2026-01-28.md")'),
|
|
9500
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).'),
|
|
9501
9590
|
frontmatter: z14.record(z14.any()).default({}).describe("Frontmatter fields (JSON object)"),
|
|
9502
9591
|
overwrite: z14.boolean().default(false).describe("If true, overwrite existing file"),
|
|
9503
9592
|
commit: z14.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
|
|
@@ -9507,7 +9596,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9507
9596
|
agent_id: z14.string().optional().describe("Agent identifier for multi-agent scoping"),
|
|
9508
9597
|
session_id: z14.string().optional().describe("Session identifier for conversation scoping")
|
|
9509
9598
|
},
|
|
9510
|
-
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 }) => {
|
|
9511
9600
|
try {
|
|
9512
9601
|
if (!validatePath(vaultPath2, notePath)) {
|
|
9513
9602
|
return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
|
|
@@ -9519,9 +9608,29 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9519
9608
|
}
|
|
9520
9609
|
const dir = path18.dirname(fullPath);
|
|
9521
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
|
+
}
|
|
9522
9631
|
const warnings = [];
|
|
9523
9632
|
const noteName = path18.basename(notePath, ".md");
|
|
9524
|
-
const existingAliases = Array.isArray(
|
|
9633
|
+
const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
|
|
9525
9634
|
const preflight = checkPreflightSimilarity(noteName);
|
|
9526
9635
|
if (preflight.existingEntity) {
|
|
9527
9636
|
warnings.push({
|
|
@@ -9545,7 +9654,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9545
9654
|
suggestion: `This may cause ambiguous wikilink resolution`
|
|
9546
9655
|
});
|
|
9547
9656
|
}
|
|
9548
|
-
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(
|
|
9657
|
+
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(effectiveContent, skipWikilinks, notePath);
|
|
9549
9658
|
let suggestInfo;
|
|
9550
9659
|
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
9551
9660
|
const result = suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
|
|
@@ -9554,21 +9663,21 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9554
9663
|
suggestInfo = `Suggested: ${result.suggestions.join(", ")}`;
|
|
9555
9664
|
}
|
|
9556
9665
|
}
|
|
9557
|
-
let finalFrontmatter =
|
|
9666
|
+
let finalFrontmatter = effectiveFrontmatter;
|
|
9558
9667
|
if (agent_id || session_id) {
|
|
9559
|
-
finalFrontmatter = injectMutationMetadata(
|
|
9668
|
+
finalFrontmatter = injectMutationMetadata(effectiveFrontmatter, { agent_id, session_id });
|
|
9560
9669
|
}
|
|
9561
9670
|
await writeVaultFile(vaultPath2, notePath, processedContent, finalFrontmatter);
|
|
9562
9671
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Create]");
|
|
9563
9672
|
const infoLines = [wikilinkInfo, suggestInfo].filter(Boolean);
|
|
9564
9673
|
const previewLines = [
|
|
9565
|
-
`Frontmatter fields: ${Object.keys(
|
|
9674
|
+
`Frontmatter fields: ${Object.keys(effectiveFrontmatter).join(", ") || "none"}`,
|
|
9566
9675
|
`Content length: ${processedContent.length} chars`
|
|
9567
9676
|
];
|
|
9568
9677
|
if (infoLines.length > 0) {
|
|
9569
9678
|
previewLines.push(`(${infoLines.join("; ")})`);
|
|
9570
9679
|
}
|
|
9571
|
-
const hasAliases =
|
|
9680
|
+
const hasAliases = effectiveFrontmatter && "aliases" in effectiveFrontmatter;
|
|
9572
9681
|
if (!hasAliases) {
|
|
9573
9682
|
const aliasSuggestions = suggestAliases(noteName, existingAliases);
|
|
9574
9683
|
if (aliasSuggestions.length > 0) {
|
|
@@ -11471,6 +11580,109 @@ function registerPolicyTools(server2, vaultPath2) {
|
|
|
11471
11580
|
);
|
|
11472
11581
|
}
|
|
11473
11582
|
|
|
11583
|
+
// src/resources/vault.ts
|
|
11584
|
+
function registerVaultResources(server2, getIndex) {
|
|
11585
|
+
server2.registerResource(
|
|
11586
|
+
"vault-stats",
|
|
11587
|
+
"vault://stats",
|
|
11588
|
+
{
|
|
11589
|
+
title: "Vault Statistics",
|
|
11590
|
+
description: "Overview of vault size, tags, links, and orphan count",
|
|
11591
|
+
mimeType: "application/json"
|
|
11592
|
+
},
|
|
11593
|
+
async (uri) => {
|
|
11594
|
+
const index = getIndex();
|
|
11595
|
+
if (!index) {
|
|
11596
|
+
return {
|
|
11597
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Index not ready" }) }]
|
|
11598
|
+
};
|
|
11599
|
+
}
|
|
11600
|
+
const noteCount = index.notes.size;
|
|
11601
|
+
const tagCount = index.tags.size;
|
|
11602
|
+
let totalLinks = 0;
|
|
11603
|
+
for (const note of index.notes.values()) {
|
|
11604
|
+
totalLinks += note.outlinks.length;
|
|
11605
|
+
}
|
|
11606
|
+
let orphanCount = 0;
|
|
11607
|
+
for (const note of index.notes.values()) {
|
|
11608
|
+
const normalizedTitle = note.title.toLowerCase();
|
|
11609
|
+
const backlinks = index.backlinks.get(normalizedTitle);
|
|
11610
|
+
if (!backlinks || backlinks.length === 0) {
|
|
11611
|
+
orphanCount++;
|
|
11612
|
+
}
|
|
11613
|
+
}
|
|
11614
|
+
const stats = {
|
|
11615
|
+
note_count: noteCount,
|
|
11616
|
+
tag_count: tagCount,
|
|
11617
|
+
total_links: totalLinks,
|
|
11618
|
+
orphan_count: orphanCount,
|
|
11619
|
+
index_built_at: index.builtAt.toISOString()
|
|
11620
|
+
};
|
|
11621
|
+
return {
|
|
11622
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(stats, null, 2) }]
|
|
11623
|
+
};
|
|
11624
|
+
}
|
|
11625
|
+
);
|
|
11626
|
+
server2.registerResource(
|
|
11627
|
+
"vault-schema",
|
|
11628
|
+
"vault://schema",
|
|
11629
|
+
{
|
|
11630
|
+
title: "Vault Schema",
|
|
11631
|
+
description: "Frontmatter field summary: field names, types, frequency",
|
|
11632
|
+
mimeType: "application/json"
|
|
11633
|
+
},
|
|
11634
|
+
async (uri) => {
|
|
11635
|
+
const index = getIndex();
|
|
11636
|
+
if (!index) {
|
|
11637
|
+
return {
|
|
11638
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Index not ready" }) }]
|
|
11639
|
+
};
|
|
11640
|
+
}
|
|
11641
|
+
const schema = getFrontmatterSchema(index);
|
|
11642
|
+
const compact = {
|
|
11643
|
+
total_notes: schema.total_notes,
|
|
11644
|
+
notes_with_frontmatter: schema.notes_with_frontmatter,
|
|
11645
|
+
field_count: schema.field_count,
|
|
11646
|
+
fields: schema.fields.map((f) => ({
|
|
11647
|
+
name: f.name,
|
|
11648
|
+
types: f.types,
|
|
11649
|
+
count: f.count,
|
|
11650
|
+
examples: f.examples.slice(0, 3)
|
|
11651
|
+
}))
|
|
11652
|
+
};
|
|
11653
|
+
return {
|
|
11654
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(compact, null, 2) }]
|
|
11655
|
+
};
|
|
11656
|
+
}
|
|
11657
|
+
);
|
|
11658
|
+
server2.registerResource(
|
|
11659
|
+
"vault-recent",
|
|
11660
|
+
"vault://recent",
|
|
11661
|
+
{
|
|
11662
|
+
title: "Recently Modified Notes",
|
|
11663
|
+
description: "Last 10 modified notes in the vault",
|
|
11664
|
+
mimeType: "application/json"
|
|
11665
|
+
},
|
|
11666
|
+
async (uri) => {
|
|
11667
|
+
const index = getIndex();
|
|
11668
|
+
if (!index) {
|
|
11669
|
+
return {
|
|
11670
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Index not ready" }) }]
|
|
11671
|
+
};
|
|
11672
|
+
}
|
|
11673
|
+
const notes = Array.from(index.notes.values()).sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, 10).map((n) => ({
|
|
11674
|
+
path: n.path,
|
|
11675
|
+
title: n.title,
|
|
11676
|
+
modified: n.modified.toISOString(),
|
|
11677
|
+
tags: n.tags
|
|
11678
|
+
}));
|
|
11679
|
+
return {
|
|
11680
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ recent_notes: notes }, null, 2) }]
|
|
11681
|
+
};
|
|
11682
|
+
}
|
|
11683
|
+
);
|
|
11684
|
+
}
|
|
11685
|
+
|
|
11474
11686
|
// src/index.ts
|
|
11475
11687
|
var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
11476
11688
|
var vaultIndex;
|
|
@@ -11661,6 +11873,7 @@ registerNoteTools(server, vaultPath, () => vaultIndex);
|
|
|
11661
11873
|
registerMoveNoteTools(server, vaultPath);
|
|
11662
11874
|
registerSystemTools2(server, vaultPath);
|
|
11663
11875
|
registerPolicyTools(server, vaultPath);
|
|
11876
|
+
registerVaultResources(server, () => vaultIndex ?? null);
|
|
11664
11877
|
console.error(`[Memory] Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
11665
11878
|
async function main() {
|
|
11666
11879
|
console.error(`[Memory] Starting Flywheel Memory server...`);
|
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",
|