@velvetmonkey/flywheel-memory 2.0.78 → 2.0.79

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 +240 -238
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -121,7 +121,7 @@ function isEmptyPlaceholder(line) {
121
121
  const trimmed = line.trim();
122
122
  return EMPTY_PLACEHOLDER_PATTERNS.some((p) => p.test(trimmed));
123
123
  }
124
- function extractHeadings2(content) {
124
+ function extractHeadings3(content) {
125
125
  const lines = content.split("\n");
126
126
  const headings = [];
127
127
  let inCodeBlock = false;
@@ -144,7 +144,7 @@ function extractHeadings2(content) {
144
144
  return headings;
145
145
  }
146
146
  function findSection(content, sectionName) {
147
- const headings = extractHeadings2(content);
147
+ const headings = extractHeadings3(content);
148
148
  const lines = content.split("\n");
149
149
  const normalizedSearch = sectionName.replace(/^#+\s*/, "").trim().toLowerCase();
150
150
  const headingIndex = headings.findIndex(
@@ -1707,6 +1707,14 @@ function extractWikilinks(content) {
1707
1707
  }
1708
1708
  return links;
1709
1709
  }
1710
+ function extractHeadings(markdown) {
1711
+ const headings = [];
1712
+ for (const line of markdown.split("\n")) {
1713
+ const match = line.match(/^(#{1,6})\s+(.+)/);
1714
+ if (match) headings.push(match[2].trim());
1715
+ }
1716
+ return headings;
1717
+ }
1710
1718
  function extractTags(content, frontmatter) {
1711
1719
  const tags = /* @__PURE__ */ new Set();
1712
1720
  const fmTags = frontmatter.tags;
@@ -1808,6 +1816,7 @@ async function parseNoteWithWarnings(file) {
1808
1816
  frontmatter,
1809
1817
  outlinks: extractWikilinks(markdown),
1810
1818
  tags: extractTags(markdown, frontmatter),
1819
+ headings: extractHeadings(markdown),
1811
1820
  modified: file.modified,
1812
1821
  created
1813
1822
  },
@@ -7213,7 +7222,7 @@ function searchFTS5(_vaultPath, query, limit = 10) {
7213
7222
  SELECT
7214
7223
  path,
7215
7224
  title,
7216
- snippet(notes_fts, 3, '<mark>', '</mark>', '...', 20) as snippet
7225
+ snippet(notes_fts, 3, '<mark>', '</mark>', '...', 64) as snippet
7217
7226
  FROM notes_fts
7218
7227
  WHERE notes_fts MATCH ?
7219
7228
  ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
@@ -7231,6 +7240,19 @@ function searchFTS5(_vaultPath, query, limit = 10) {
7231
7240
  function getFTS5State() {
7232
7241
  return { ...state };
7233
7242
  }
7243
+ function getContentPreview(notePath, maxChars = 300) {
7244
+ if (!db2) return null;
7245
+ try {
7246
+ const row = db2.prepare(
7247
+ "SELECT substr(content, 1, ?) as preview FROM notes_fts WHERE path = ?"
7248
+ ).get(maxChars + 50, notePath);
7249
+ if (!row?.preview) return null;
7250
+ const truncated = row.preview.length > maxChars ? row.preview.slice(0, maxChars).replace(/\s\S*$/, "") + "..." : row.preview;
7251
+ return truncated;
7252
+ } catch {
7253
+ return null;
7254
+ }
7255
+ }
7234
7256
  function countFTS5Mentions(term) {
7235
7257
  if (!db2) return 0;
7236
7258
  try {
@@ -9655,7 +9677,16 @@ function enrichResult(result, index, stateDb2) {
9655
9677
  enriched.tags = note.tags;
9656
9678
  enriched.aliases = note.aliases;
9657
9679
  enriched.backlink_count = backlinks.length;
9680
+ enriched.backlinks = backlinks.map((bl) => ({ source: bl.source, line: bl.line }));
9658
9681
  enriched.outlink_count = note.outlinks.length;
9682
+ enriched.outlinks = note.outlinks.map((l) => {
9683
+ const targetLower = l.target.toLowerCase();
9684
+ const exists = index.entities.has(targetLower);
9685
+ const out = { target: l.target, line: l.line, exists };
9686
+ if (l.alias) out.alias = l.alias;
9687
+ return out;
9688
+ });
9689
+ enriched.headings = note.headings || [];
9659
9690
  enriched.modified = note.modified.toISOString();
9660
9691
  if (note.created) enriched.created = note.created.toISOString();
9661
9692
  }
@@ -9670,6 +9701,10 @@ function enrichResult(result, index, stateDb2) {
9670
9701
  } catch {
9671
9702
  }
9672
9703
  }
9704
+ if (!result.snippet) {
9705
+ const preview = getContentPreview(result.path);
9706
+ if (preview) enriched.content_preview = preview;
9707
+ }
9673
9708
  return enriched;
9674
9709
  }
9675
9710
  function sortNotes(notes, sortBy, order) {
@@ -9696,7 +9731,7 @@ function sortNotes(notes, sortBy, order) {
9696
9731
  function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
9697
9732
  server2.tool(
9698
9733
  "search",
9699
- '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. When embeddings have been built (via init_semantic), content and all scopes automatically include embedding-based results via hybrid ranking.\n\nExample: search({ query: "quarterly review", scope: "content", limit: 5 })\nExample: search({ where: { type: "project", status: "active" }, scope: "metadata" })',
9734
+ 'Search the vault \u2014 always try this before reading files. Returns frontmatter, backlinks (with lines), outlinks (with lines + exists), headings, content snippet or preview, entity metadata, and timestamps for every hit.\n\nSearch 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. When embeddings have been built (via init_semantic), content and all scopes automatically include embedding-based results via hybrid ranking.\n\nExample: search({ query: "quarterly review", scope: "content", limit: 5 })\nExample: search({ where: { type: "project", status: "active" }, scope: "metadata" })',
9700
9735
  {
9701
9736
  query: z4.string().optional().describe('Search query text. Required for scope "content", "entities", "all". For "metadata" scope, use filters instead.'),
9702
9737
  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). Semantic results are automatically included when embeddings have been built (via init_semantic)."),
@@ -9783,14 +9818,10 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
9783
9818
  matchingNotes = sortNotes(matchingNotes, sort_by ?? "modified", order ?? "desc");
9784
9819
  const totalMatches = matchingNotes.length;
9785
9820
  const limitedNotes = matchingNotes.slice(0, limit);
9786
- const notes = limitedNotes.map((note) => ({
9787
- path: note.path,
9788
- title: note.title,
9789
- modified: note.modified.toISOString(),
9790
- created: note.created?.toISOString(),
9791
- tags: note.tags,
9792
- frontmatter: note.frontmatter
9793
- }));
9821
+ const stateDb2 = getStateDb();
9822
+ const notes = limitedNotes.map(
9823
+ (note) => enrichResult({ path: note.path, title: note.title }, index, stateDb2)
9824
+ );
9794
9825
  if (scope === "metadata" || hasMetadataFilters || totalMatches > 0) {
9795
9826
  return { content: [{ type: "text", text: JSON.stringify({
9796
9827
  scope: "metadata",
@@ -10290,104 +10321,6 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
10290
10321
  };
10291
10322
  }
10292
10323
  );
10293
- const GetNoteMetadataOutputSchema = {
10294
- path: z5.string().describe("Path to the note"),
10295
- title: z5.string().describe("Note title"),
10296
- exists: z5.boolean().describe("Whether the note exists"),
10297
- frontmatter: z5.record(z5.unknown()).describe("Frontmatter properties"),
10298
- tags: z5.array(z5.string()).describe("Tags on this note"),
10299
- aliases: z5.array(z5.string()).describe("Aliases for this note"),
10300
- outlink_count: z5.number().describe("Number of outgoing links"),
10301
- backlink_count: z5.number().describe("Number of incoming links"),
10302
- word_count: z5.number().optional().describe("Approximate word count"),
10303
- created: z5.string().optional().describe("Created date (ISO format)"),
10304
- modified: z5.string().describe("Last modified date (ISO format)")
10305
- };
10306
- server2.registerTool(
10307
- "get_note_metadata",
10308
- {
10309
- title: "Get Note Metadata",
10310
- description: "Get metadata about a note (frontmatter, tags, link counts) without reading full content. Useful for quick analysis.",
10311
- inputSchema: {
10312
- path: z5.string().describe("Path to the note"),
10313
- include_word_count: z5.boolean().default(false).describe("Count words (requires reading file)")
10314
- },
10315
- outputSchema: GetNoteMetadataOutputSchema
10316
- },
10317
- async ({
10318
- path: notePath,
10319
- include_word_count
10320
- }) => {
10321
- requireIndex();
10322
- const index = getIndex();
10323
- const vaultPath2 = getVaultPath();
10324
- let resolvedPath = notePath;
10325
- if (!notePath.endsWith(".md")) {
10326
- const resolved = index.entities.get(notePath.toLowerCase());
10327
- if (resolved) {
10328
- resolvedPath = resolved;
10329
- } else {
10330
- resolvedPath = notePath + ".md";
10331
- }
10332
- }
10333
- const note = index.notes.get(resolvedPath);
10334
- if (!note) {
10335
- const output2 = {
10336
- path: resolvedPath,
10337
- title: resolvedPath.replace(/\.md$/, "").split("/").pop() || "",
10338
- exists: false,
10339
- frontmatter: {},
10340
- tags: [],
10341
- aliases: [],
10342
- outlink_count: 0,
10343
- backlink_count: 0,
10344
- modified: (/* @__PURE__ */ new Date()).toISOString()
10345
- };
10346
- return {
10347
- content: [
10348
- {
10349
- type: "text",
10350
- text: JSON.stringify(output2, null, 2)
10351
- }
10352
- ],
10353
- structuredContent: output2
10354
- };
10355
- }
10356
- const normalizedPath = resolvedPath.toLowerCase().replace(/\.md$/, "");
10357
- const backlinks = index.backlinks.get(normalizedPath) || [];
10358
- let wordCount;
10359
- if (include_word_count) {
10360
- try {
10361
- const fullPath = path14.join(vaultPath2, resolvedPath);
10362
- const content = await fs11.promises.readFile(fullPath, "utf-8");
10363
- wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
10364
- } catch {
10365
- }
10366
- }
10367
- const output = {
10368
- path: note.path,
10369
- title: note.title,
10370
- exists: true,
10371
- frontmatter: note.frontmatter,
10372
- tags: note.tags,
10373
- aliases: note.aliases,
10374
- outlink_count: note.outlinks.length,
10375
- backlink_count: backlinks.length,
10376
- word_count: wordCount,
10377
- created: note.created?.toISOString(),
10378
- modified: note.modified.toISOString()
10379
- };
10380
- return {
10381
- content: [
10382
- {
10383
- type: "text",
10384
- text: JSON.stringify(output, null, 2)
10385
- }
10386
- ],
10387
- structuredContent: output
10388
- };
10389
- }
10390
- );
10391
10324
  const GetFolderStructureOutputSchema = {
10392
10325
  folder_count: z5.number().describe("Total number of folders"),
10393
10326
  folders: z5.array(
@@ -10591,7 +10524,7 @@ import { z as z6 } from "zod";
10591
10524
  import * as fs12 from "fs";
10592
10525
  import * as path15 from "path";
10593
10526
  var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
10594
- function extractHeadings(content) {
10527
+ function extractHeadings2(content) {
10595
10528
  const lines = content.split("\n");
10596
10529
  const headings = [];
10597
10530
  let inCodeBlock = false;
@@ -10651,7 +10584,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
10651
10584
  return null;
10652
10585
  }
10653
10586
  const lines = content.split("\n");
10654
- const headings = extractHeadings(content);
10587
+ const headings = extractHeadings2(content);
10655
10588
  const sections = buildSections(headings, lines.length);
10656
10589
  const contentWithoutCode = content.replace(/```[\s\S]*?```/g, "");
10657
10590
  const words = contentWithoutCode.split(/\s+/).filter((w) => w.length > 0);
@@ -10674,7 +10607,7 @@ async function getSectionContent(index, notePath, headingText, vaultPath2, inclu
10674
10607
  return null;
10675
10608
  }
10676
10609
  const lines = content.split("\n");
10677
- const headings = extractHeadings(content);
10610
+ const headings = extractHeadings2(content);
10678
10611
  const targetHeading = headings.find(
10679
10612
  (h) => h.text.toLowerCase() === headingText.toLowerCase()
10680
10613
  );
@@ -10715,7 +10648,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
10715
10648
  } catch {
10716
10649
  continue;
10717
10650
  }
10718
- const headings = extractHeadings(content);
10651
+ const headings = extractHeadings2(content);
10719
10652
  for (const heading of headings) {
10720
10653
  if (regex.test(heading.text)) {
10721
10654
  results.push({
@@ -10737,7 +10670,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
10737
10670
  "get_note_structure",
10738
10671
  {
10739
10672
  title: "Get Note Structure",
10740
- description: "Get the heading structure and sections of a note. Returns headings, sections hierarchy, word count, and line count.",
10673
+ description: "Read the structure of a specific note. Use after search identifies a note you need more detail on. Returns headings, frontmatter, tags, word count. Set include_content: true to get the full markdown.",
10741
10674
  inputSchema: {
10742
10675
  path: z6.string().describe("Path to the note"),
10743
10676
  include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
@@ -13324,7 +13257,7 @@ function ensureSectionExists(content, section, notePath) {
13324
13257
  if (boundary) {
13325
13258
  return { boundary };
13326
13259
  }
13327
- const headings = extractHeadings2(content);
13260
+ const headings = extractHeadings3(content);
13328
13261
  let message;
13329
13262
  if (headings.length === 0) {
13330
13263
  message = `Section '${section}' not found. This file has no headings. Add section structure (## Heading) to enable section-scoped mutations.`;
@@ -19387,59 +19320,58 @@ var watcherStatus = null;
19387
19320
  function getWatcherStatus() {
19388
19321
  return watcherStatus;
19389
19322
  }
19390
- var PRESETS = {
19391
- // Presets
19392
- minimal: ["search", "structure", "append", "frontmatter", "notes"],
19393
- writer: ["search", "structure", "append", "frontmatter", "notes", "tasks"],
19394
- agent: ["search", "structure", "append", "frontmatter", "notes", "memory"],
19395
- researcher: ["search", "structure", "backlinks", "hubs", "paths"],
19396
- full: [
19397
- "search",
19398
- "backlinks",
19399
- "orphans",
19400
- "hubs",
19401
- "paths",
19402
- "schema",
19403
- "structure",
19404
- "tasks",
19405
- "health",
19406
- "wikilinks",
19407
- "append",
19408
- "frontmatter",
19409
- "notes",
19410
- "note-ops",
19411
- "git",
19412
- "policy",
19413
- "memory"
19414
- ],
19415
- // Composable bundles
19416
- graph: ["backlinks", "orphans", "hubs", "paths"],
19417
- analysis: ["schema", "wikilinks"],
19418
- tasks: ["tasks"],
19419
- health: ["health"],
19420
- ops: ["git", "policy"],
19421
- "note-ops": ["note-ops"]
19422
- };
19423
19323
  var ALL_CATEGORIES = [
19424
- "backlinks",
19425
- "orphans",
19426
- "hubs",
19427
- "paths",
19428
19324
  "search",
19325
+ "read",
19326
+ "write",
19327
+ "graph",
19429
19328
  "schema",
19430
- "structure",
19431
- "tasks",
19432
- "health",
19433
19329
  "wikilinks",
19434
- "append",
19435
- "frontmatter",
19436
- "notes",
19330
+ "corrections",
19331
+ "tasks",
19332
+ "memory",
19437
19333
  "note-ops",
19438
- "git",
19439
- "policy",
19440
- "memory"
19334
+ "diagnostics",
19335
+ "automation"
19441
19336
  ];
19442
- var DEFAULT_PRESET = "minimal";
19337
+ var PRESETS = {
19338
+ // Presets
19339
+ default: ["search", "read", "write", "tasks"],
19340
+ agent: ["search", "read", "write", "memory"],
19341
+ full: ALL_CATEGORIES,
19342
+ // Composable bundles (one per category)
19343
+ graph: ["graph"],
19344
+ schema: ["schema"],
19345
+ wikilinks: ["wikilinks"],
19346
+ corrections: ["corrections"],
19347
+ tasks: ["tasks"],
19348
+ memory: ["memory"],
19349
+ "note-ops": ["note-ops"],
19350
+ diagnostics: ["diagnostics"],
19351
+ automation: ["automation"]
19352
+ };
19353
+ var DEFAULT_PRESET = "default";
19354
+ var DEPRECATED_ALIASES = {
19355
+ minimal: "default",
19356
+ writer: "default",
19357
+ // writer was default+tasks, now default includes tasks
19358
+ researcher: "default",
19359
+ // use default,graph for graph exploration
19360
+ backlinks: "graph",
19361
+ // get_backlinks moved to graph
19362
+ structure: "read",
19363
+ append: "write",
19364
+ frontmatter: "write",
19365
+ notes: "write",
19366
+ orphans: "graph",
19367
+ hubs: "graph",
19368
+ paths: "graph",
19369
+ health: "diagnostics",
19370
+ analysis: "wikilinks",
19371
+ git: "automation",
19372
+ ops: "automation",
19373
+ policy: "automation"
19374
+ };
19443
19375
  function parseEnabledCategories() {
19444
19376
  const envValue = (process.env.FLYWHEEL_TOOLS ?? process.env.FLYWHEEL_PRESET)?.trim();
19445
19377
  if (!envValue) {
@@ -19449,13 +19381,25 @@ function parseEnabledCategories() {
19449
19381
  if (PRESETS[lowerValue]) {
19450
19382
  return new Set(PRESETS[lowerValue]);
19451
19383
  }
19384
+ if (DEPRECATED_ALIASES[lowerValue]) {
19385
+ const resolved = DEPRECATED_ALIASES[lowerValue];
19386
+ serverLog("server", `Preset "${lowerValue}" is deprecated \u2014 use "${resolved}" instead`, "warn");
19387
+ if (PRESETS[resolved]) {
19388
+ return new Set(PRESETS[resolved]);
19389
+ }
19390
+ return /* @__PURE__ */ new Set([resolved]);
19391
+ }
19452
19392
  const categories = /* @__PURE__ */ new Set();
19453
19393
  for (const item of envValue.split(",")) {
19454
- const category = item.trim().toLowerCase();
19455
- if (ALL_CATEGORIES.includes(category)) {
19456
- categories.add(category);
19457
- } else if (PRESETS[category]) {
19458
- for (const c of PRESETS[category]) {
19394
+ const raw = item.trim().toLowerCase();
19395
+ const resolved = DEPRECATED_ALIASES[raw] ?? raw;
19396
+ if (resolved !== raw) {
19397
+ serverLog("server", `Category "${raw}" is deprecated \u2014 use "${resolved}" instead`, "warn");
19398
+ }
19399
+ if (ALL_CATEGORIES.includes(resolved)) {
19400
+ categories.add(resolved);
19401
+ } else if (PRESETS[resolved]) {
19402
+ for (const c of PRESETS[resolved]) {
19459
19403
  categories.add(c);
19460
19404
  }
19461
19405
  } else {
@@ -19470,88 +19414,146 @@ function parseEnabledCategories() {
19470
19414
  }
19471
19415
  var enabledCategories = parseEnabledCategories();
19472
19416
  var TOOL_CATEGORY = {
19473
- // health (includes periodic detection in output)
19474
- health_check: "health",
19475
- get_vault_stats: "health",
19476
- get_folder_structure: "health",
19477
- refresh_index: "health",
19478
- // absorbed rebuild_search_index
19479
- get_all_entities: "health",
19480
- list_entities: "hubs",
19481
- get_unlinked_mentions: "health",
19482
- // search (unified: metadata + content + entities)
19417
+ // search (3 tools)
19483
19418
  search: "search",
19484
19419
  init_semantic: "search",
19485
- // backlinks
19486
- get_backlinks: "backlinks",
19487
- get_forward_links: "backlinks",
19488
- // orphans (graph_analysis covers orphans, dead_ends, sources, hubs, stale)
19489
- graph_analysis: "orphans",
19490
- get_connection_strength: "hubs",
19491
- // paths
19492
- get_link_path: "paths",
19493
- get_common_neighbors: "paths",
19494
- // schema (vault_schema + note_intelligence cover all schema tools)
19420
+ find_similar: "search",
19421
+ // read (3 tools) — note reading
19422
+ get_note_structure: "read",
19423
+ get_section_content: "read",
19424
+ find_sections: "read",
19425
+ // write (5 tools) — content mutations + frontmatter + note creation
19426
+ vault_add_to_section: "write",
19427
+ vault_remove_from_section: "write",
19428
+ vault_replace_in_section: "write",
19429
+ vault_update_frontmatter: "write",
19430
+ vault_create_note: "write",
19431
+ // graph (9 tools) — structural analysis + link detail
19432
+ graph_analysis: "graph",
19433
+ get_backlinks: "graph",
19434
+ get_forward_links: "graph",
19435
+ get_connection_strength: "graph",
19436
+ list_entities: "graph",
19437
+ get_link_path: "graph",
19438
+ get_common_neighbors: "graph",
19439
+ get_weighted_links: "graph",
19440
+ get_strong_connections: "graph",
19441
+ // schema (5 tools) — schema intelligence + migrations
19495
19442
  vault_schema: "schema",
19496
19443
  note_intelligence: "schema",
19497
- // structure (absorbed get_headings + vault_list_sections)
19498
- get_note_structure: "structure",
19499
- get_section_content: "structure",
19500
- find_sections: "structure",
19501
- get_note_metadata: "structure",
19502
- // tasks (unified: all task queries + write)
19444
+ rename_field: "schema",
19445
+ migrate_field_values: "schema",
19446
+ rename_tag: "schema",
19447
+ // wikilinks (7 tools) — suggestions, validation, discovery
19448
+ suggest_wikilinks: "wikilinks",
19449
+ validate_links: "wikilinks",
19450
+ wikilink_feedback: "wikilinks",
19451
+ discover_stub_candidates: "wikilinks",
19452
+ discover_cooccurrence_gaps: "wikilinks",
19453
+ suggest_entity_aliases: "wikilinks",
19454
+ unlinked_mentions_report: "wikilinks",
19455
+ // corrections (4 tools)
19456
+ vault_record_correction: "corrections",
19457
+ vault_list_corrections: "corrections",
19458
+ vault_resolve_correction: "corrections",
19459
+ absorb_as_alias: "corrections",
19460
+ // tasks (3 tools)
19503
19461
  tasks: "tasks",
19504
19462
  vault_toggle_task: "tasks",
19505
19463
  vault_add_task: "tasks",
19506
- // wikilinks
19507
- suggest_wikilinks: "wikilinks",
19508
- validate_links: "wikilinks",
19509
- // append (content mutations)
19510
- vault_add_to_section: "append",
19511
- vault_remove_from_section: "append",
19512
- vault_replace_in_section: "append",
19513
- // frontmatter (absorbed vault_add_frontmatter_field via only_if_missing)
19514
- vault_update_frontmatter: "frontmatter",
19515
- // notes (create only)
19516
- vault_create_note: "notes",
19517
- // note-ops (file management)
19464
+ // memory (3 tools) — agent working memory
19465
+ memory: "memory",
19466
+ recall: "memory",
19467
+ brief: "memory",
19468
+ // note-ops (4 tools) — file management
19518
19469
  vault_delete_note: "note-ops",
19519
19470
  vault_move_note: "note-ops",
19520
19471
  vault_rename_note: "note-ops",
19521
- // git
19522
- vault_undo_last_mutation: "git",
19523
- // policy
19524
- policy: "policy",
19525
- // schema (migrations + tag rename)
19526
- rename_field: "schema",
19527
- migrate_field_values: "schema",
19528
- rename_tag: "schema",
19529
- // health (growth metrics)
19530
- vault_growth: "health",
19531
- // wikilinks (feedback)
19532
- wikilink_feedback: "wikilinks",
19533
- // health (activity tracking)
19534
- vault_activity: "health",
19535
- // schema (content similarity)
19536
- find_similar: "schema",
19537
- // health (config management)
19538
- flywheel_config: "health",
19539
- // health (server activity log)
19540
- server_log: "health",
19541
- // health (merge suggestions)
19542
- suggest_entity_merges: "health",
19543
- dismiss_merge_suggestion: "health",
19544
- // note-ops (entity merge)
19545
19472
  merge_entities: "note-ops",
19546
- // memory (agent working memory)
19547
- memory: "memory",
19548
- recall: "memory",
19549
- brief: "memory"
19473
+ // diagnostics (13 tools) — vault health, stats, config, activity
19474
+ health_check: "diagnostics",
19475
+ get_vault_stats: "diagnostics",
19476
+ get_folder_structure: "diagnostics",
19477
+ refresh_index: "diagnostics",
19478
+ get_all_entities: "diagnostics",
19479
+ get_unlinked_mentions: "diagnostics",
19480
+ vault_growth: "diagnostics",
19481
+ vault_activity: "diagnostics",
19482
+ flywheel_config: "diagnostics",
19483
+ server_log: "diagnostics",
19484
+ suggest_entity_merges: "diagnostics",
19485
+ dismiss_merge_suggestion: "diagnostics",
19486
+ vault_init: "diagnostics",
19487
+ // automation (2 tools) — git undo + policy engine
19488
+ vault_undo_last_mutation: "automation",
19489
+ policy: "automation"
19550
19490
  };
19551
- var server = new McpServer({
19552
- name: "flywheel-memory",
19553
- version: pkg.version
19554
- });
19491
+ function generateInstructions(categories) {
19492
+ const parts = [];
19493
+ parts.push(`Flywheel provides tools to search, read, and write an Obsidian vault's knowledge graph.
19494
+
19495
+ Tool selection:
19496
+ 1. "search" is the primary tool. Each result includes: frontmatter, tags, aliases,
19497
+ backlinks (with line numbers), outlinks (with line numbers and existence check),
19498
+ headings, content snippet or preview, entity category, hub score, and timestamps.
19499
+ This is usually enough to answer without reading any files.
19500
+ 2. Escalate to "get_note_structure" only when you need the full markdown content
19501
+ or word count. Use "get_section_content" to read one section by heading name.
19502
+ 3. Use vault write tools instead of raw file writes \u2014 they auto-link entities
19503
+ and commit changes.`);
19504
+ if (categories.has("read")) {
19505
+ parts.push(`
19506
+ ## Read
19507
+
19508
+ Escalation: "search" (enriched metadata + content preview) \u2192 "get_note_structure"
19509
+ (full content + word count) \u2192 "get_section_content" (single section).
19510
+ "find_sections" finds headings across the vault by pattern.`);
19511
+ }
19512
+ if (categories.has("write")) {
19513
+ parts.push(`
19514
+ ## Write
19515
+
19516
+ Write to existing notes with "vault_add_to_section". Create new notes with "vault_create_note". Update metadata with "vault_update_frontmatter". All writes auto-link entities \u2014 no manual [[wikilinks]] needed.`);
19517
+ }
19518
+ if (categories.has("memory")) {
19519
+ parts.push(`
19520
+ ## Memory
19521
+
19522
+ Session workflow: call "brief" at conversation start for vault context (recent sessions, active entities, stored memories). Use "recall" before answering questions \u2014 it searches entities, notes, and memories with graph-boosted ranking. Use "memory" to store observations that should persist across sessions.`);
19523
+ }
19524
+ if (categories.has("graph")) {
19525
+ parts.push(`
19526
+ ## Graph
19527
+
19528
+ Use "get_backlinks" for per-backlink surrounding text (reads source files).
19529
+ Use "get_forward_links" for resolved file paths and alias text.
19530
+ Use "graph_analysis" for structural queries (hubs, orphans, dead ends).
19531
+ Use "get_connection_strength" to measure link strength between notes.
19532
+ Use "get_link_path" to find shortest paths.`);
19533
+ }
19534
+ if (categories.has("tasks")) {
19535
+ parts.push(`
19536
+ ## Tasks
19537
+
19538
+ Use "tasks" to query tasks across the vault (filter by status, due date, path). Use "vault_add_task" to create tasks and "vault_toggle_task" to complete them.`);
19539
+ }
19540
+ if (categories.has("schema")) {
19541
+ parts.push(`
19542
+ ## Schema
19543
+
19544
+ Use "vault_schema" before bulk operations to understand field conventions, inconsistencies, and note types. Use "note_intelligence" for per-note analysis.`);
19545
+ }
19546
+ return parts.join("\n");
19547
+ }
19548
+ var server = new McpServer(
19549
+ {
19550
+ name: "flywheel-memory",
19551
+ version: pkg.version
19552
+ },
19553
+ {
19554
+ instructions: generateInstructions(enabledCategories)
19555
+ }
19556
+ );
19555
19557
  var _registeredCount = 0;
19556
19558
  var _skippedCount = 0;
19557
19559
  function gateByCategory(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.78",
3
+ "version": "2.0.79",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 51 tools for search, backlinks, graph queries, mutations, agent memory, and hybrid semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",