@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.
Files changed (2) hide show
  1. package/dist/index.js +118 -9
  2. 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
- "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).",
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
- "Toggle a task checkbox between checked and unchecked",
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
- "Add a new task to a section in a markdown note",
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
- "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).",
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
- "Create a new note in the vault with optional frontmatter and content",
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.8",
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.8",
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",