@velvetmonkey/flywheel-memory 2.0.8 → 2.0.9
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 +118 -9
- 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) {
|
|
@@ -6140,7 +6141,7 @@ function sortNotes(notes, sortBy, order) {
|
|
|
6140
6141
|
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
6141
6142
|
server2.tool(
|
|
6142
6143
|
"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
|
|
6144
|
+
'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
6145
|
{
|
|
6145
6146
|
query: z4.string().optional().describe('Search query text. Required for scope "content", "entities", "all". For "metadata" scope, use filters instead.'),
|
|
6146
6147
|
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)"),
|
|
@@ -7411,7 +7412,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath) {
|
|
|
7411
7412
|
"graph_analysis",
|
|
7412
7413
|
{
|
|
7413
7414
|
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',
|
|
7415
|
+
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
7416
|
inputSchema: {
|
|
7416
7417
|
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale"]).describe("Type of graph analysis to perform"),
|
|
7417
7418
|
folder: z8.string().optional().describe("Limit to notes in this folder (orphans, dead_ends, sources)"),
|
|
@@ -7984,7 +7985,7 @@ function registerVaultSchemaTools(server2, getIndex, getVaultPath) {
|
|
|
7984
7985
|
"vault_schema",
|
|
7985
7986
|
{
|
|
7986
7987
|
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',
|
|
7988
|
+
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
7989
|
inputSchema: {
|
|
7989
7990
|
analysis: z9.enum([
|
|
7990
7991
|
"overview",
|
|
@@ -8554,7 +8555,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
|
|
|
8554
8555
|
"note_intelligence",
|
|
8555
8556
|
{
|
|
8556
8557
|
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',
|
|
8558
|
+
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
8559
|
inputSchema: {
|
|
8559
8560
|
analysis: z10.enum([
|
|
8560
8561
|
"prose_patterns",
|
|
@@ -9031,7 +9032,9 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
9031
9032
|
function registerMutationTools(server2, vaultPath2, getConfig = () => ({})) {
|
|
9032
9033
|
server2.tool(
|
|
9033
9034
|
"vault_add_to_section",
|
|
9034
|
-
|
|
9035
|
+
`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).
|
|
9036
|
+
|
|
9037
|
+
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
9038
|
{
|
|
9036
9039
|
path: z11.string().describe('Vault-relative path to the note (e.g., "daily-notes/2026-01-28.md")'),
|
|
9037
9040
|
section: z11.string().describe('Heading text to add to (e.g., "Log" or "## Log")'),
|
|
@@ -9295,7 +9298,7 @@ function toggleTask(content, lineNumber) {
|
|
|
9295
9298
|
function registerTaskTools(server2, vaultPath2) {
|
|
9296
9299
|
server2.tool(
|
|
9297
9300
|
"vault_toggle_task",
|
|
9298
|
-
|
|
9301
|
+
'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
9302
|
{
|
|
9300
9303
|
path: z12.string().describe("Vault-relative path to the note"),
|
|
9301
9304
|
task: z12.string().describe("Task text to find (partial match supported)"),
|
|
@@ -9355,7 +9358,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
9355
9358
|
);
|
|
9356
9359
|
server2.tool(
|
|
9357
9360
|
"vault_add_task",
|
|
9358
|
-
|
|
9361
|
+
'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
9362
|
{
|
|
9360
9363
|
path: z12.string().describe("Vault-relative path to the note"),
|
|
9361
9364
|
section: z12.string().describe("Section to add the task to"),
|
|
@@ -9433,7 +9436,9 @@ import { z as z13 } from "zod";
|
|
|
9433
9436
|
function registerFrontmatterTools(server2, vaultPath2) {
|
|
9434
9437
|
server2.tool(
|
|
9435
9438
|
"vault_update_frontmatter",
|
|
9436
|
-
|
|
9439
|
+
`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).
|
|
9440
|
+
|
|
9441
|
+
Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { status: "active", priority: 1 }, only_if_missing: true })`,
|
|
9437
9442
|
{
|
|
9438
9443
|
path: z13.string().describe("Vault-relative path to the note"),
|
|
9439
9444
|
frontmatter: z13.record(z13.any()).describe("Frontmatter fields to update (JSON object)"),
|
|
@@ -9494,7 +9499,7 @@ import path18 from "path";
|
|
|
9494
9499
|
function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
9495
9500
|
server2.tool(
|
|
9496
9501
|
"vault_create_note",
|
|
9497
|
-
|
|
9502
|
+
'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
9503
|
{
|
|
9499
9504
|
path: z14.string().describe('Vault-relative path for the new note (e.g., "daily-notes/2026-01-28.md")'),
|
|
9500
9505
|
content: z14.string().default("").describe("Initial content for the note"),
|
|
@@ -11471,6 +11476,109 @@ function registerPolicyTools(server2, vaultPath2) {
|
|
|
11471
11476
|
);
|
|
11472
11477
|
}
|
|
11473
11478
|
|
|
11479
|
+
// src/resources/vault.ts
|
|
11480
|
+
function registerVaultResources(server2, getIndex) {
|
|
11481
|
+
server2.registerResource(
|
|
11482
|
+
"vault-stats",
|
|
11483
|
+
"vault://stats",
|
|
11484
|
+
{
|
|
11485
|
+
title: "Vault Statistics",
|
|
11486
|
+
description: "Overview of vault size, tags, links, and orphan count",
|
|
11487
|
+
mimeType: "application/json"
|
|
11488
|
+
},
|
|
11489
|
+
async (uri) => {
|
|
11490
|
+
const index = getIndex();
|
|
11491
|
+
if (!index) {
|
|
11492
|
+
return {
|
|
11493
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Index not ready" }) }]
|
|
11494
|
+
};
|
|
11495
|
+
}
|
|
11496
|
+
const noteCount = index.notes.size;
|
|
11497
|
+
const tagCount = index.tags.size;
|
|
11498
|
+
let totalLinks = 0;
|
|
11499
|
+
for (const note of index.notes.values()) {
|
|
11500
|
+
totalLinks += note.outlinks.length;
|
|
11501
|
+
}
|
|
11502
|
+
let orphanCount = 0;
|
|
11503
|
+
for (const note of index.notes.values()) {
|
|
11504
|
+
const normalizedTitle = note.title.toLowerCase();
|
|
11505
|
+
const backlinks = index.backlinks.get(normalizedTitle);
|
|
11506
|
+
if (!backlinks || backlinks.length === 0) {
|
|
11507
|
+
orphanCount++;
|
|
11508
|
+
}
|
|
11509
|
+
}
|
|
11510
|
+
const stats = {
|
|
11511
|
+
note_count: noteCount,
|
|
11512
|
+
tag_count: tagCount,
|
|
11513
|
+
total_links: totalLinks,
|
|
11514
|
+
orphan_count: orphanCount,
|
|
11515
|
+
index_built_at: index.builtAt.toISOString()
|
|
11516
|
+
};
|
|
11517
|
+
return {
|
|
11518
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(stats, null, 2) }]
|
|
11519
|
+
};
|
|
11520
|
+
}
|
|
11521
|
+
);
|
|
11522
|
+
server2.registerResource(
|
|
11523
|
+
"vault-schema",
|
|
11524
|
+
"vault://schema",
|
|
11525
|
+
{
|
|
11526
|
+
title: "Vault Schema",
|
|
11527
|
+
description: "Frontmatter field summary: field names, types, frequency",
|
|
11528
|
+
mimeType: "application/json"
|
|
11529
|
+
},
|
|
11530
|
+
async (uri) => {
|
|
11531
|
+
const index = getIndex();
|
|
11532
|
+
if (!index) {
|
|
11533
|
+
return {
|
|
11534
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Index not ready" }) }]
|
|
11535
|
+
};
|
|
11536
|
+
}
|
|
11537
|
+
const schema = getFrontmatterSchema(index);
|
|
11538
|
+
const compact = {
|
|
11539
|
+
total_notes: schema.total_notes,
|
|
11540
|
+
notes_with_frontmatter: schema.notes_with_frontmatter,
|
|
11541
|
+
field_count: schema.field_count,
|
|
11542
|
+
fields: schema.fields.map((f) => ({
|
|
11543
|
+
name: f.name,
|
|
11544
|
+
types: f.types,
|
|
11545
|
+
count: f.count,
|
|
11546
|
+
examples: f.examples.slice(0, 3)
|
|
11547
|
+
}))
|
|
11548
|
+
};
|
|
11549
|
+
return {
|
|
11550
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(compact, null, 2) }]
|
|
11551
|
+
};
|
|
11552
|
+
}
|
|
11553
|
+
);
|
|
11554
|
+
server2.registerResource(
|
|
11555
|
+
"vault-recent",
|
|
11556
|
+
"vault://recent",
|
|
11557
|
+
{
|
|
11558
|
+
title: "Recently Modified Notes",
|
|
11559
|
+
description: "Last 10 modified notes in the vault",
|
|
11560
|
+
mimeType: "application/json"
|
|
11561
|
+
},
|
|
11562
|
+
async (uri) => {
|
|
11563
|
+
const index = getIndex();
|
|
11564
|
+
if (!index) {
|
|
11565
|
+
return {
|
|
11566
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Index not ready" }) }]
|
|
11567
|
+
};
|
|
11568
|
+
}
|
|
11569
|
+
const notes = Array.from(index.notes.values()).sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, 10).map((n) => ({
|
|
11570
|
+
path: n.path,
|
|
11571
|
+
title: n.title,
|
|
11572
|
+
modified: n.modified.toISOString(),
|
|
11573
|
+
tags: n.tags
|
|
11574
|
+
}));
|
|
11575
|
+
return {
|
|
11576
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ recent_notes: notes }, null, 2) }]
|
|
11577
|
+
};
|
|
11578
|
+
}
|
|
11579
|
+
);
|
|
11580
|
+
}
|
|
11581
|
+
|
|
11474
11582
|
// src/index.ts
|
|
11475
11583
|
var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
11476
11584
|
var vaultIndex;
|
|
@@ -11661,6 +11769,7 @@ registerNoteTools(server, vaultPath, () => vaultIndex);
|
|
|
11661
11769
|
registerMoveNoteTools(server, vaultPath);
|
|
11662
11770
|
registerSystemTools2(server, vaultPath);
|
|
11663
11771
|
registerPolicyTools(server, vaultPath);
|
|
11772
|
+
registerVaultResources(server, () => vaultIndex ?? null);
|
|
11664
11773
|
console.error(`[Memory] Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
11665
11774
|
async function main() {
|
|
11666
11775
|
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.9",
|
|
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.9",
|
|
54
54
|
"better-sqlite3": "^11.0.0",
|
|
55
55
|
"chokidar": "^4.0.0",
|
|
56
56
|
"gray-matter": "^4.0.3",
|