@velvetmonkey/flywheel-memory 2.0.148 → 2.0.150

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 +179 -576
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -642,9 +642,6 @@ function loadNoteEmbeddingsForPaths(paths) {
642
642
  }
643
643
  return result;
644
644
  }
645
- function getEntityEmbedding(entityName) {
646
- return getEmbMap().get(entityName) ?? null;
647
- }
648
645
  function getEntityEmbeddingsCount() {
649
646
  const db4 = getDb();
650
647
  if (!db4) return 0;
@@ -3396,7 +3393,8 @@ async function rebuildIndex(vaultPath2) {
3396
3393
  console.error(`[Flywheel] Scanning vault for entities...`);
3397
3394
  const startTime = Date.now();
3398
3395
  entityIndex = await scanVaultEntities(vaultPath2, {
3399
- excludeFolders: DEFAULT_EXCLUDE_FOLDERS
3396
+ excludeFolders: DEFAULT_EXCLUDE_FOLDERS,
3397
+ customCategories: getConfig()?.custom_categories
3400
3398
  });
3401
3399
  indexReady = true;
3402
3400
  lastLoadedAt = Date.now();
@@ -4921,7 +4919,7 @@ function formatContent(content, format) {
4921
4919
  const lines = trimmed.split("\n");
4922
4920
  return lines.map((line, i) => {
4923
4921
  if (i === 0) return `- ${line}`;
4924
- if (line === "") return "";
4922
+ if (line === "") return " ";
4925
4923
  return ` ${line}`;
4926
4924
  }).join("\n");
4927
4925
  }
@@ -4932,7 +4930,7 @@ function formatContent(content, format) {
4932
4930
  const lines = trimmed.split("\n");
4933
4931
  return lines.map((line, i) => {
4934
4932
  if (i === 0) return `- [ ] ${line}`;
4935
- if (line === "") return "";
4933
+ if (line === "") return " ";
4936
4934
  return ` ${line}`;
4937
4935
  }).join("\n");
4938
4936
  }
@@ -4943,7 +4941,7 @@ function formatContent(content, format) {
4943
4941
  const lines = trimmed.split("\n");
4944
4942
  return lines.map((line, i) => {
4945
4943
  if (i === 0) return `1. ${line}`;
4946
- if (line === "") return "";
4944
+ if (line === "") return " ";
4947
4945
  return ` ${line}`;
4948
4946
  }).join("\n");
4949
4947
  }
@@ -4959,7 +4957,7 @@ function formatContent(content, format) {
4959
4957
  const indent = " ";
4960
4958
  return lines.map((line, i) => {
4961
4959
  if (i === 0) return `${prefix}${line}`;
4962
- if (line === "") return "";
4960
+ if (line === "") return indent;
4963
4961
  return `${indent}${line}`;
4964
4962
  }).join("\n");
4965
4963
  }
@@ -5036,7 +5034,7 @@ function insertInSection(content, section, newContent, position, options) {
5036
5034
  if (indent) {
5037
5035
  const contentLines = formattedContent.split("\n");
5038
5036
  const indentedContent = contentLines.map((line) => {
5039
- if (line === "") return line;
5037
+ if (line === "") return indent || line;
5040
5038
  return indent + line;
5041
5039
  }).join("\n");
5042
5040
  lines.splice(section.contentStartLine, 0, indentedContent);
@@ -5059,7 +5057,7 @@ function insertInSection(content, section, newContent, position, options) {
5059
5057
  const indent = detectSectionBaseIndentation(lines, section.contentStartLine, section.endLine);
5060
5058
  const contentLines = formattedContent.split("\n");
5061
5059
  const indentedContent = contentLines.map((line) => {
5062
- if (line === "") return line;
5060
+ if (line === "") return indent || line;
5063
5061
  return indent + line;
5064
5062
  }).join("\n");
5065
5063
  lines[lastContentLineIdx] = indentedContent;
@@ -5082,7 +5080,7 @@ function insertInSection(content, section, newContent, position, options) {
5082
5080
  const indent = detectSectionBaseIndentation(lines, section.contentStartLine, section.endLine);
5083
5081
  const contentLines = formattedContent.split("\n");
5084
5082
  const indentedContent = contentLines.map((line) => {
5085
- if (line === "") return line;
5083
+ if (line === "") return indent || line;
5086
5084
  return indent + line;
5087
5085
  }).join("\n");
5088
5086
  lines.splice(insertLine, 0, indentedContent);
@@ -10718,9 +10716,8 @@ var ALL_CATEGORIES = [
10718
10716
  ];
10719
10717
  var PRESETS = {
10720
10718
  // Presets
10721
- default: ["search", "read", "write", "tasks"],
10722
- agent: ["search", "read", "write", "memory"],
10723
- full: ALL_CATEGORIES.filter((c) => c !== "memory"),
10719
+ default: ["search", "read", "write", "tasks", "memory"],
10720
+ full: [...ALL_CATEGORIES],
10724
10721
  // Composable bundles (one per category)
10725
10722
  graph: ["graph"],
10726
10723
  schema: ["schema"],
@@ -10734,6 +10731,8 @@ var PRESETS = {
10734
10731
  };
10735
10732
  var DEFAULT_PRESET = "default";
10736
10733
  var DEPRECATED_ALIASES = {
10734
+ agent: "default",
10735
+ // agent merged into default — memory now included
10737
10736
  minimal: "default",
10738
10737
  writer: "default",
10739
10738
  // writer was default+tasks, now default includes tasks
@@ -10848,9 +10847,8 @@ var TOOL_CATEGORY = {
10848
10847
  tasks: "tasks",
10849
10848
  vault_toggle_task: "tasks",
10850
10849
  vault_add_task: "tasks",
10851
- // memory (3 tools) -- agent working memory
10850
+ // memory (2 tools) -- session memory
10852
10851
  memory: "memory",
10853
- recall: "memory",
10854
10852
  brief: "memory",
10855
10853
  // note-ops (4 tools) -- file management
10856
10854
  vault_delete_note: "note-ops",
@@ -10889,10 +10887,12 @@ function generateInstructions(categories, registry) {
10889
10887
  parts.push(`Flywheel provides tools to search, read, and write an Obsidian vault's knowledge graph.
10890
10888
 
10891
10889
  Tool selection:
10892
- 1. "search" is the primary tool. Each result includes: frontmatter, tags, aliases,
10893
- backlinks (with line numbers), outlinks (with line numbers and existence check),
10894
- headings, content snippet or preview, entity category, hub score, and timestamps.
10895
- This is usually enough to answer without reading any files.
10890
+ 1. "search" is the primary tool. One call searches notes, entities, and memories.
10891
+ Each result carries: type (note/entity/memory), frontmatter, tags, aliases,
10892
+ backlinks (ranked by edge weight \xD7 recency), outlinks (existence-checked),
10893
+ section provenance, extracted dates, entity bridges, confidence scores,
10894
+ content snippet or preview, entity category, hub score, and timestamps.
10895
+ This is a decision surface \u2014 usually enough to answer without reading any files.
10896
10896
  2. Escalate to "get_note_structure" only when you need the full markdown content
10897
10897
  or word count. Use "get_section_content" to read one section by heading name.
10898
10898
  3. Start with a broad search: just query text, no filters. Only add folder, tag,
@@ -10900,7 +10900,7 @@ Tool selection:
10900
10900
  if (!hasEmbeddingsIndex()) {
10901
10901
  parts.push(`
10902
10902
  **Setup:** Run \`init_semantic\` once to build embeddings. This unlocks hybrid search (BM25 + semantic),
10903
- improves recall results, and enables similarity-based tools. Without it, search is keyword-only.`);
10903
+ improves search results, and enables similarity-based tools. Without it, search is keyword-only.`);
10904
10904
  }
10905
10905
  if (registry?.isMultiVault) {
10906
10906
  parts.push(`
@@ -10915,9 +10915,9 @@ This server manages multiple vaults. Every tool has an optional "vault" paramete
10915
10915
  **Frontmatter matters more than content** for Flywheel's intelligence. When creating or updating notes, always set:
10916
10916
  - \`type:\` \u2014 drives entity categorization (person, project, technology). Without it, the category is guessed from the name alone and is often wrong.
10917
10917
  - \`aliases:\` \u2014 alternative names so the entity is found when referred to differently. Without it, the entity is invisible to searches using alternate names.
10918
- - \`description:\` \u2014 one-line summary shown in search results and used by recall. Without it, search results and recall are degraded.
10918
+ - \`description:\` \u2014 one-line summary shown in search results and used for entity ranking. Without it, search quality is degraded.
10919
10919
  - Tags \u2014 used for filtering, suggestion scoring, and schema analysis.
10920
- Good frontmatter is the highest-leverage action for improving suggestions, recall, and link quality.`);
10920
+ Good frontmatter is the highest-leverage action for improving suggestions, search, and link quality.`);
10921
10921
  if (categories.has("read")) {
10922
10922
  parts.push(`
10923
10923
  ## Read
@@ -10968,7 +10968,10 @@ you say "run the weekly review for this week".`);
10968
10968
  parts.push(`
10969
10969
  ## Memory
10970
10970
 
10971
- 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" with action "store" to save observations, facts, or context that should persist across sessions (e.g. key decisions, user preferences, project status).`);
10971
+ "brief" delivers startup context (recent sessions, active entities, stored memories) \u2014 call it at
10972
+ conversation start. "search" finds everything \u2014 notes, entities, and memories in one call. "memory"
10973
+ with action "store" persists observations, facts, or preferences across sessions (e.g. key decisions,
10974
+ user preferences, project status).`);
10972
10975
  }
10973
10976
  if (categories.has("graph")) {
10974
10977
  parts.push(`
@@ -11013,7 +11016,7 @@ import * as path37 from "path";
11013
11016
  import { dirname as dirname5, join as join19 } from "path";
11014
11017
  import { statSync as statSync6, readFileSync as readFileSync5 } from "fs";
11015
11018
  import { fileURLToPath } from "url";
11016
- import { z as z40 } from "zod";
11019
+ import { z as z39 } from "zod";
11017
11020
  import { getSessionId } from "@velvetmonkey/vault-core";
11018
11021
  init_vault_scope();
11019
11022
 
@@ -13851,64 +13854,6 @@ function enrichResultLight(result, index, stateDb2) {
13851
13854
  }
13852
13855
  return enriched;
13853
13856
  }
13854
- function enrichEntityCompact(entityName, stateDb2, index) {
13855
- const enriched = {};
13856
- if (stateDb2) {
13857
- try {
13858
- const entity = getEntityByName2(stateDb2, entityName);
13859
- if (entity) {
13860
- enriched.category = entity.category;
13861
- enriched.hub_score = entity.hubScore;
13862
- if (entity.aliases.length > 0) enriched.aliases = entity.aliases;
13863
- enriched.path = entity.path;
13864
- }
13865
- } catch {
13866
- }
13867
- }
13868
- if (index) {
13869
- const entityPath = enriched.path ?? index.entities.get(entityName.toLowerCase());
13870
- if (entityPath) {
13871
- const note = index.notes.get(entityPath);
13872
- const normalizedPath = entityPath.toLowerCase().replace(/\.md$/, "");
13873
- const backlinks = index.backlinks.get(normalizedPath) || [];
13874
- enriched.backlink_count = backlinks.length;
13875
- if (note) {
13876
- if (Object.keys(note.frontmatter).length > 0) enriched.frontmatter = note.frontmatter;
13877
- if (note.tags.length > 0) enriched.tags = note.tags;
13878
- if (note.outlinks.length > 0) {
13879
- enriched.outlink_names = getOutlinkNames(note.outlinks, entityPath, index, stateDb2, COMPACT_OUTLINK_NAMES);
13880
- }
13881
- }
13882
- }
13883
- }
13884
- return enriched;
13885
- }
13886
- function enrichNoteCompact(notePath, stateDb2, index) {
13887
- const enriched = {};
13888
- if (!index) return enriched;
13889
- const note = index.notes.get(notePath);
13890
- if (!note) return enriched;
13891
- const normalizedPath = notePath.toLowerCase().replace(/\.md$/, "");
13892
- const backlinks = index.backlinks.get(normalizedPath) || [];
13893
- if (Object.keys(note.frontmatter).length > 0) enriched.frontmatter = note.frontmatter;
13894
- if (note.tags.length > 0) enriched.tags = note.tags;
13895
- enriched.backlink_count = backlinks.length;
13896
- enriched.modified = note.modified.toISOString();
13897
- if (note.outlinks.length > 0) {
13898
- enriched.outlink_names = getOutlinkNames(note.outlinks, notePath, index, stateDb2, COMPACT_OUTLINK_NAMES);
13899
- }
13900
- if (stateDb2) {
13901
- try {
13902
- const entity = getEntityByName2(stateDb2, note.title);
13903
- if (entity) {
13904
- enriched.category = entity.category;
13905
- enriched.hub_score = entity.hubScore;
13906
- }
13907
- } catch {
13908
- }
13909
- }
13910
- return enriched;
13911
- }
13912
13857
 
13913
13858
  // src/core/read/multihop.ts
13914
13859
  import { getEntityByName as getEntityByName3, searchEntities } from "@velvetmonkey/vault-core";
@@ -14381,7 +14326,7 @@ function sortNotes(notes, sortBy, order) {
14381
14326
  function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
14382
14327
  server2.tool(
14383
14328
  "search",
14384
- 'Search the vault \u2014 always start with just a query, no filters. Top results get full metadata (frontmatter, top backlinks/outlinks ranked by edge weight + recency); remaining results get lightweight summaries. Narrow with filters only if the broad search returns too many irrelevant results. Use get_note_structure for headings/full structure, get_backlinks for complete backlink lists.\n\nSearches across content (FTS5 full-text + hybrid semantic), entities (people/projects/technologies), and metadata (frontmatter/tags/folders). Hybrid semantic results are automatically included when embeddings have been built (via init_semantic).\n\nExample: search({ query: "quarterly review", limit: 5 })\nExample: search({ where: { type: "project", status: "active" } })\n\nMulti-vault: when configured with multiple vaults, omitting the `vault` parameter searches all vaults and merges results (each result includes a `vault` field). Pass `vault` to search a specific vault.',
14329
+ 'Search everything \u2014 notes, entities, and memories \u2014 in one call. Returns a decision surface with three sections: note results (with section provenance, dates, bridges, confidence), matching entity profiles, and relevant memories.\n\nNote results carry full metadata (frontmatter, scored backlinks/outlinks, snippets). Start with just a query, no filters. Narrow with filters only if needed. Use get_note_structure for full content, get_section_content to read one section.\n\nSearches note content (FTS5 + hybrid semantic), entity profiles (people, projects, technologies), and stored memories. Hybrid results included automatically when embeddings are built (via init_semantic).\n\nExample: search({ query: "quarterly review", limit: 5 })\nExample: search({ where: { type: "project", status: "active" } })\n\nMulti-vault: omitting `vault` searches all vaults and merges results. Pass `vault` to search a specific vault.',
14385
14330
  {
14386
14331
  query: z5.string().optional().describe("Search query text. Required unless using metadata filters (where, has_tag, folder, etc.)"),
14387
14332
  // Metadata filters
@@ -14765,6 +14710,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
14765
14710
  const stateDb2 = getStateDb3?.();
14766
14711
  if (stateDb2) {
14767
14712
  try {
14713
+ const config = loadConfig(stateDb2);
14768
14714
  const entityIndex2 = await scanVaultEntities2(vaultPath2, {
14769
14715
  excludeFolders: [
14770
14716
  "daily-notes",
@@ -14786,7 +14732,8 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
14786
14732
  "articles",
14787
14733
  "bookmarks",
14788
14734
  "web-clips"
14789
- ]
14735
+ ],
14736
+ customCategories: config.custom_categories
14790
14737
  });
14791
14738
  stateDb2.replaceAllEntities(entityIndex2);
14792
14739
  console.error(`[Flywheel] Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
@@ -21768,410 +21715,8 @@ function registerMemoryTools(server2, getStateDb3) {
21768
21715
  );
21769
21716
  }
21770
21717
 
21771
- // src/tools/read/recall.ts
21772
- import { z as z26 } from "zod";
21773
- import { searchEntities as searchEntitiesDb2 } from "@velvetmonkey/vault-core";
21774
- init_recency();
21775
- init_cooccurrence();
21776
- init_wikilinks();
21777
- init_edgeWeights();
21778
- init_wikilinkFeedback();
21779
- init_embeddings();
21780
- init_stemmer();
21781
-
21782
- // src/core/read/mmr.ts
21783
- init_embeddings();
21784
- function selectByMmr(candidates, limit, lambda = 0.7) {
21785
- if (candidates.length <= limit) return candidates;
21786
- if (candidates.length === 0) return [];
21787
- const maxScore = Math.max(...candidates.map((c) => c.score));
21788
- if (maxScore === 0) return candidates.slice(0, limit);
21789
- const normScores = /* @__PURE__ */ new Map();
21790
- for (const c of candidates) {
21791
- normScores.set(c.id, c.score / maxScore);
21792
- }
21793
- const selected = [];
21794
- const remaining = new Set(candidates.map((_, i) => i));
21795
- let bestIdx = 0;
21796
- let bestScore = -Infinity;
21797
- for (const idx of remaining) {
21798
- if (candidates[idx].score > bestScore) {
21799
- bestScore = candidates[idx].score;
21800
- bestIdx = idx;
21801
- }
21802
- }
21803
- selected.push(candidates[bestIdx]);
21804
- remaining.delete(bestIdx);
21805
- while (selected.length < limit && remaining.size > 0) {
21806
- let bestMmr = -Infinity;
21807
- let bestCandidate = -1;
21808
- for (const idx of remaining) {
21809
- const candidate = candidates[idx];
21810
- const relevance = normScores.get(candidate.id) || 0;
21811
- let maxSim = 0;
21812
- if (candidate.embedding !== null) {
21813
- for (const sel of selected) {
21814
- if (sel.embedding !== null) {
21815
- const sim = cosineSimilarity(candidate.embedding, sel.embedding);
21816
- if (sim > maxSim) maxSim = sim;
21817
- }
21818
- }
21819
- }
21820
- const mmr = lambda * relevance - (1 - lambda) * maxSim;
21821
- if (mmr > bestMmr) {
21822
- bestMmr = mmr;
21823
- bestCandidate = idx;
21824
- }
21825
- }
21826
- if (bestCandidate === -1) break;
21827
- selected.push(candidates[bestCandidate]);
21828
- remaining.delete(bestCandidate);
21829
- }
21830
- return selected;
21831
- }
21832
-
21833
- // src/tools/read/recall.ts
21834
- function scoreTextRelevance(query, content) {
21835
- const queryTokens = tokenize(query).map((t) => t.toLowerCase());
21836
- const queryStems = queryTokens.map((t) => stem(t));
21837
- const contentLower = content.toLowerCase();
21838
- const contentTokens = new Set(tokenize(contentLower));
21839
- const contentStems = new Set([...contentTokens].map((t) => stem(t)));
21840
- let score = 0;
21841
- for (let i = 0; i < queryTokens.length; i++) {
21842
- const token = queryTokens[i];
21843
- const stemmed = queryStems[i];
21844
- if (contentTokens.has(token)) {
21845
- score += 10;
21846
- } else if (contentStems.has(stemmed)) {
21847
- score += 5;
21848
- }
21849
- }
21850
- if (contentLower.includes(query.toLowerCase())) {
21851
- score += 15;
21852
- }
21853
- return score;
21854
- }
21855
- function getEdgeWeightBoost(entityName, edgeWeightMap) {
21856
- const avgWeight = edgeWeightMap.get(entityName.toLowerCase());
21857
- if (!avgWeight || avgWeight <= 1) return 0;
21858
- return Math.min((avgWeight - 1) * 3, 6);
21859
- }
21860
- async function performRecall(stateDb2, query, options = {}) {
21861
- const {
21862
- max_results = 20,
21863
- focus,
21864
- entity,
21865
- max_tokens,
21866
- diversity = 1,
21867
- vaultPath: vaultPath2
21868
- } = options;
21869
- const results = [];
21870
- const recencyIndex2 = loadRecencyFromStateDb();
21871
- const edgeWeightMap = getEntityEdgeWeightMap(stateDb2);
21872
- const feedbackBoosts = getAllFeedbackBoosts(stateDb2);
21873
- const cooccurrenceIndex2 = getCooccurrenceIndex();
21874
- if (!focus || focus === "entities") {
21875
- try {
21876
- const entityResults = searchEntitiesDb2(stateDb2, query, max_results);
21877
- for (const e of entityResults) {
21878
- const textScore = scoreTextRelevance(query, `${e.name} ${e.description || ""}`);
21879
- const recency = recencyIndex2 ? getRecencyBoost(e.name, recencyIndex2) : 0;
21880
- const feedback = feedbackBoosts.get(e.name) ?? 0;
21881
- const edgeWeight = getEdgeWeightBoost(e.name, edgeWeightMap);
21882
- const total = textScore + recency + feedback + edgeWeight;
21883
- if (total > 0) {
21884
- results.push({
21885
- type: "entity",
21886
- id: e.name,
21887
- content: e.description || `Entity: ${e.name} (${e.category})`,
21888
- score: total,
21889
- breakdown: {
21890
- textRelevance: textScore,
21891
- recencyBoost: recency,
21892
- cooccurrenceBoost: 0,
21893
- feedbackBoost: feedback,
21894
- edgeWeightBoost: edgeWeight,
21895
- semanticBoost: 0
21896
- }
21897
- });
21898
- }
21899
- }
21900
- } catch {
21901
- }
21902
- }
21903
- if (!focus || focus === "notes") {
21904
- try {
21905
- const noteResults = searchFTS5("", query, max_results);
21906
- for (const n of noteResults) {
21907
- const textScore = Math.max(10, scoreTextRelevance(query, `${n.title || ""} ${n.snippet || ""}`));
21908
- const entityName = n.title || n.path.replace(/\.md$/, "").split("/").pop() || "";
21909
- const recency = recencyIndex2 ? getRecencyBoost(entityName, recencyIndex2) : 0;
21910
- const feedback = feedbackBoosts.get(entityName) ?? 0;
21911
- const edgeWeight = getEdgeWeightBoost(entityName, edgeWeightMap);
21912
- const total = textScore + recency + feedback + edgeWeight;
21913
- results.push({
21914
- type: "note",
21915
- id: n.path,
21916
- content: n.snippet || n.title || n.path,
21917
- score: total,
21918
- breakdown: {
21919
- textRelevance: textScore,
21920
- recencyBoost: recency,
21921
- cooccurrenceBoost: 0,
21922
- // applied in post-pass below
21923
- feedbackBoost: feedback,
21924
- edgeWeightBoost: edgeWeight,
21925
- semanticBoost: 0
21926
- }
21927
- });
21928
- }
21929
- } catch {
21930
- }
21931
- }
21932
- if (!focus || focus === "memories") {
21933
- try {
21934
- const memResults = searchMemories(stateDb2, {
21935
- query,
21936
- entity,
21937
- limit: max_results
21938
- });
21939
- const now = Date.now();
21940
- for (const m of memResults) {
21941
- const textScore = scoreTextRelevance(query, `${m.key} ${m.value}`);
21942
- const confidenceBoost = m.confidence * 5;
21943
- let typeBoost = 0;
21944
- switch (m.memory_type) {
21945
- case "fact":
21946
- typeBoost = 3;
21947
- break;
21948
- case "preference":
21949
- typeBoost = 2;
21950
- break;
21951
- case "observation": {
21952
- const ageDays = (now - m.updated_at) / 864e5;
21953
- const recencyFactor = Math.max(0.2, 1 - ageDays / 7);
21954
- typeBoost = 1 + 4 * recencyFactor;
21955
- break;
21956
- }
21957
- case "summary":
21958
- typeBoost = 1;
21959
- break;
21960
- }
21961
- const memScore = textScore + confidenceBoost + typeBoost;
21962
- results.push({
21963
- type: "memory",
21964
- id: m.key,
21965
- content: m.value,
21966
- score: memScore,
21967
- breakdown: {
21968
- textRelevance: textScore,
21969
- recencyBoost: typeBoost,
21970
- // type-aware boost reported as recency
21971
- cooccurrenceBoost: 0,
21972
- feedbackBoost: confidenceBoost,
21973
- edgeWeightBoost: 0,
21974
- semanticBoost: 0
21975
- }
21976
- });
21977
- }
21978
- } catch {
21979
- }
21980
- }
21981
- if (cooccurrenceIndex2) {
21982
- const seedEntities = /* @__PURE__ */ new Set();
21983
- for (const r of results) {
21984
- if (r.type === "entity") {
21985
- seedEntities.add(r.id);
21986
- } else if (r.type === "note") {
21987
- const name = r.id.replace(/\.md$/, "").split("/").pop() || "";
21988
- if (name) seedEntities.add(name);
21989
- }
21990
- }
21991
- for (const r of results) {
21992
- if (r.type === "entity" || r.type === "note") {
21993
- const name = r.type === "entity" ? r.id : r.id.replace(/\.md$/, "").split("/").pop() || "";
21994
- const boost = getCooccurrenceBoost(name, seedEntities, cooccurrenceIndex2, recencyIndex2);
21995
- if (boost > 0) {
21996
- r.breakdown.cooccurrenceBoost = boost;
21997
- r.score += boost;
21998
- }
21999
- }
22000
- }
22001
- }
22002
- if ((!focus || focus === "entities") && query.length >= 20 && hasEntityEmbeddingsIndex()) {
22003
- try {
22004
- const embedding = await embedTextCached(query);
22005
- const semanticMatches = findSemanticallySimilarEntities(embedding, max_results);
22006
- for (const match of semanticMatches) {
22007
- if (match.similarity < 0.3) continue;
22008
- const boost = match.similarity * 15;
22009
- const existing = results.find((r) => r.type === "entity" && r.id === match.entityName);
22010
- if (existing) {
22011
- existing.score += boost;
22012
- existing.breakdown.semanticBoost = boost;
22013
- } else {
22014
- results.push({
22015
- type: "entity",
22016
- id: match.entityName,
22017
- content: `Semantically similar to: "${query}"`,
22018
- score: boost,
22019
- breakdown: {
22020
- textRelevance: 0,
22021
- recencyBoost: 0,
22022
- cooccurrenceBoost: 0,
22023
- feedbackBoost: 0,
22024
- edgeWeightBoost: 0,
22025
- semanticBoost: boost
22026
- }
22027
- });
22028
- }
22029
- }
22030
- } catch {
22031
- }
22032
- }
22033
- results.sort((a, b) => b.score - a.score);
22034
- const seen = /* @__PURE__ */ new Set();
22035
- const deduped = results.filter((r) => {
22036
- const key = `${r.type}:${r.id}`;
22037
- if (seen.has(key)) return false;
22038
- seen.add(key);
22039
- return true;
22040
- });
22041
- let selected;
22042
- if (hasEmbeddingsIndex() && deduped.length > max_results) {
22043
- const notePaths = deduped.filter((r) => r.type === "note").map((r) => r.id);
22044
- const noteEmbeddings = loadNoteEmbeddingsForPaths(notePaths);
22045
- let queryEmbedding = null;
22046
- const mmrCandidates = [];
22047
- for (const r of deduped) {
22048
- let embedding = null;
22049
- if (r.type === "entity") {
22050
- embedding = getEntityEmbedding(r.id);
22051
- } else if (r.type === "note") {
22052
- embedding = noteEmbeddings.get(r.id) ?? null;
22053
- } else if (r.type === "memory") {
22054
- try {
22055
- if (!queryEmbedding) queryEmbedding = await embedTextCached(query);
22056
- embedding = await embedTextCached(r.content);
22057
- } catch {
22058
- }
22059
- }
22060
- mmrCandidates.push({ id: `${r.type}:${r.id}`, score: r.score, embedding });
22061
- }
22062
- const mmrSelected = selectByMmr(mmrCandidates, max_results, diversity);
22063
- const selectedIds = new Set(mmrSelected.map((m) => m.id));
22064
- selected = deduped.filter((r) => selectedIds.has(`${r.type}:${r.id}`));
22065
- const orderMap = new Map(mmrSelected.map((m, i) => [m.id, i]));
22066
- selected.sort((a, b) => (orderMap.get(`${a.type}:${a.id}`) ?? 0) - (orderMap.get(`${b.type}:${b.id}`) ?? 0));
22067
- } else {
22068
- selected = deduped.slice(0, max_results);
22069
- }
22070
- if (vaultPath2) {
22071
- const queryTokens = tokenize(query).map((t) => t.toLowerCase());
22072
- let queryEmb = null;
22073
- if (hasEmbeddingsIndex()) {
22074
- try {
22075
- queryEmb = await embedTextCached(query);
22076
- } catch {
22077
- }
22078
- }
22079
- for (const r of selected) {
22080
- if (r.type !== "note") continue;
22081
- try {
22082
- const absPath = vaultPath2 + "/" + r.id;
22083
- const snippets = await extractBestSnippets(absPath, queryEmb, queryTokens);
22084
- if (snippets.length > 0 && snippets[0].text.length > 0) {
22085
- r.content = snippets[0].text;
22086
- }
22087
- } catch {
22088
- }
22089
- }
22090
- }
22091
- if (max_tokens) {
22092
- let tokenBudget = max_tokens;
22093
- const budgeted = [];
22094
- for (const r of selected) {
22095
- const estimatedTokens = Math.ceil(r.content.length / 4);
22096
- if (tokenBudget - estimatedTokens < 0 && budgeted.length > 0) break;
22097
- tokenBudget -= estimatedTokens;
22098
- budgeted.push(r);
22099
- }
22100
- return budgeted;
22101
- }
22102
- return selected;
22103
- }
22104
- function registerRecallTools(server2, getStateDb3, getVaultPath, getIndex) {
22105
- server2.tool(
22106
- "recall",
22107
- "Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
22108
- {
22109
- query: z26.string().describe('What to recall (e.g., "Project X", "meetings about auth")'),
22110
- max_results: z26.number().min(1).max(100).optional().describe("Max results (default: 20)"),
22111
- focus: z26.enum(["entities", "notes", "memories"]).optional().describe("Limit to a specific result type. Omit for best results (searches everything)."),
22112
- entity: z26.string().optional().describe("Filter memories by entity association"),
22113
- max_tokens: z26.number().optional().describe("Token budget for response (truncates lower-ranked results)"),
22114
- diversity: z26.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
22115
- },
22116
- async (args) => {
22117
- const stateDb2 = getStateDb3();
22118
- if (!stateDb2) {
22119
- return {
22120
- content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
22121
- isError: true
22122
- };
22123
- }
22124
- const results = await performRecall(stateDb2, args.query, {
22125
- max_results: args.max_results,
22126
- focus: args.focus,
22127
- entity: args.entity,
22128
- max_tokens: args.max_tokens,
22129
- diversity: args.diversity,
22130
- vaultPath: getVaultPath?.()
22131
- });
22132
- const entities = results.filter((r) => r.type === "entity");
22133
- const notes = results.filter((r) => r.type === "note");
22134
- const memories = results.filter((r) => r.type === "memory");
22135
- const index = getIndex?.() ?? null;
22136
- const enrichedNotes = notes.map((n) => ({
22137
- path: n.id,
22138
- snippet: n.content,
22139
- ...enrichNoteCompact(n.id, stateDb2, index)
22140
- }));
22141
- const hopResults = index ? multiHopBackfill(enrichedNotes, index, stateDb2, {
22142
- maxBackfill: Math.max(5, (args.max_results ?? 10) - notes.length)
22143
- }) : [];
22144
- if (index) {
22145
- const allNoteResults = [...enrichedNotes, ...hopResults];
22146
- const expansionTerms = extractExpansionTerms(allNoteResults, args.query, index);
22147
- const expansionResults = expandQuery(expansionTerms, allNoteResults, index, stateDb2);
22148
- hopResults.push(...expansionResults);
22149
- }
22150
- return {
22151
- content: [{
22152
- type: "text",
22153
- text: JSON.stringify({
22154
- query: args.query,
22155
- total: results.length,
22156
- entities: entities.map((e) => ({
22157
- name: e.id,
22158
- description: e.content,
22159
- ...enrichEntityCompact(e.id, stateDb2, index)
22160
- })),
22161
- notes: [...enrichedNotes, ...hopResults],
22162
- memories: memories.map((m) => ({
22163
- key: m.id,
22164
- value: m.content
22165
- }))
22166
- }, null, 2)
22167
- }]
22168
- };
22169
- }
22170
- );
22171
- }
22172
-
22173
21718
  // src/tools/read/brief.ts
22174
- import { z as z27 } from "zod";
21719
+ import { z as z26 } from "zod";
22175
21720
  init_corrections();
22176
21721
  function estimateTokens2(value) {
22177
21722
  const str = JSON.stringify(value);
@@ -22313,8 +21858,8 @@ function registerBriefTools(server2, getStateDb3) {
22313
21858
  "brief",
22314
21859
  "Get a startup context briefing: recent sessions, active entities, memories, pending corrections, and vault stats. Call at conversation start.",
22315
21860
  {
22316
- max_tokens: z27.number().optional().describe("Token budget (lower-priority sections truncated first)"),
22317
- focus: z27.string().optional().describe("Focus entity or topic (filters content)")
21861
+ max_tokens: z26.number().optional().describe("Token budget (lower-priority sections truncated first)"),
21862
+ focus: z26.string().optional().describe("Focus entity or topic (filters content)")
22318
21863
  },
22319
21864
  async (args) => {
22320
21865
  const stateDb2 = getStateDb3();
@@ -22365,24 +21910,24 @@ function registerBriefTools(server2, getStateDb3) {
22365
21910
  }
22366
21911
 
22367
21912
  // src/tools/write/config.ts
22368
- import { z as z28 } from "zod";
21913
+ import { z as z27 } from "zod";
22369
21914
  import { saveFlywheelConfigToDb as saveFlywheelConfigToDb2 } from "@velvetmonkey/vault-core";
22370
21915
  var VALID_CONFIG_KEYS = {
22371
- vault_name: z28.string(),
22372
- exclude_task_tags: z28.array(z28.string()),
22373
- exclude_analysis_tags: z28.array(z28.string()),
22374
- exclude_entities: z28.array(z28.string()),
22375
- exclude_entity_folders: z28.array(z28.string()),
22376
- wikilink_strictness: z28.enum(["conservative", "balanced", "aggressive"]),
22377
- implicit_detection: z28.boolean(),
22378
- implicit_patterns: z28.array(z28.string()),
22379
- adaptive_strictness: z28.boolean(),
22380
- proactive_linking: z28.boolean(),
22381
- proactive_min_score: z28.number(),
22382
- proactive_max_per_file: z28.number(),
22383
- proactive_max_per_day: z28.number(),
22384
- custom_categories: z28.record(z28.string(), z28.object({
22385
- type_boost: z28.number().optional()
21916
+ vault_name: z27.string(),
21917
+ exclude_task_tags: z27.array(z27.string()),
21918
+ exclude_analysis_tags: z27.array(z27.string()),
21919
+ exclude_entities: z27.array(z27.string()),
21920
+ exclude_entity_folders: z27.array(z27.string()),
21921
+ wikilink_strictness: z27.enum(["conservative", "balanced", "aggressive"]),
21922
+ implicit_detection: z27.boolean(),
21923
+ implicit_patterns: z27.array(z27.string()),
21924
+ adaptive_strictness: z27.boolean(),
21925
+ proactive_linking: z27.boolean(),
21926
+ proactive_min_score: z27.number(),
21927
+ proactive_max_per_file: z27.number(),
21928
+ proactive_max_per_day: z27.number(),
21929
+ custom_categories: z27.record(z27.string(), z27.object({
21930
+ type_boost: z27.number().optional()
22386
21931
  }))
22387
21932
  };
22388
21933
  function registerConfigTools(server2, getConfig2, setConfig, getStateDb3) {
@@ -22392,9 +21937,9 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb3) {
22392
21937
  title: "Flywheel Config",
22393
21938
  description: 'Read or update Flywheel configuration.\n- "get": Returns the current FlywheelConfig\n- "set": Updates a single config key and returns the updated config\n\nExample: flywheel_config({ mode: "get" })\nExample: flywheel_config({ mode: "set", key: "exclude_analysis_tags", value: ["habit", "daily"] })',
22394
21939
  inputSchema: {
22395
- mode: z28.enum(["get", "set"]).describe("Operation mode"),
22396
- key: z28.string().optional().describe("Config key to update (required for set mode)"),
22397
- value: z28.unknown().optional().describe("New value for the key (required for set mode)")
21940
+ mode: z27.enum(["get", "set"]).describe("Operation mode"),
21941
+ key: z27.string().optional().describe("Config key to update (required for set mode)"),
21942
+ value: z27.unknown().optional().describe("New value for the key (required for set mode)")
22398
21943
  }
22399
21944
  },
22400
21945
  async ({ mode, key, value }) => {
@@ -22450,7 +21995,7 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb3) {
22450
21995
  // src/tools/write/enrich.ts
22451
21996
  init_wikilinks();
22452
21997
  init_wikilinkFeedback();
22453
- import { z as z29 } from "zod";
21998
+ import { z as z28 } from "zod";
22454
21999
  import * as fs32 from "fs/promises";
22455
22000
  import * as path35 from "path";
22456
22001
  import { scanVaultEntities as scanVaultEntities3, SCHEMA_VERSION as SCHEMA_VERSION2 } from "@velvetmonkey/vault-core";
@@ -22571,7 +22116,8 @@ async function executeRun(stateDb2, vaultPath2) {
22571
22116
  if (entityCount === 0) {
22572
22117
  const start = Date.now();
22573
22118
  try {
22574
- const entityIndex2 = await scanVaultEntities3(vaultPath2, { excludeFolders: EXCLUDE_FOLDERS });
22119
+ const config = loadConfig(stateDb2);
22120
+ const entityIndex2 = await scanVaultEntities3(vaultPath2, { excludeFolders: EXCLUDE_FOLDERS, customCategories: config.custom_categories });
22575
22121
  stateDb2.replaceAllEntities(entityIndex2);
22576
22122
  const newCount = entityIndex2._metadata.total_entities;
22577
22123
  steps.push({
@@ -22754,10 +22300,10 @@ function registerInitTools(server2, getVaultPath, getStateDb3) {
22754
22300
  "vault_init",
22755
22301
  `Initialize vault for Flywheel. Modes: "status" (check what's ready/missing), "run" (execute missing init steps), "enrich" (scan notes with zero wikilinks and apply entity links).`,
22756
22302
  {
22757
- mode: z29.enum(["status", "run", "enrich"]).default("status").describe("Operation mode (default: status)"),
22758
- dry_run: z29.boolean().default(true).describe("For enrich mode: preview without modifying files (default: true)"),
22759
- batch_size: z29.number().default(50).describe("For enrich mode: max notes per invocation (default: 50)"),
22760
- offset: z29.number().default(0).describe("For enrich mode: skip this many eligible notes (for pagination)")
22303
+ mode: z28.enum(["status", "run", "enrich"]).default("status").describe("Operation mode (default: status)"),
22304
+ dry_run: z28.boolean().default(true).describe("For enrich mode: preview without modifying files (default: true)"),
22305
+ batch_size: z28.number().default(50).describe("For enrich mode: max notes per invocation (default: 50)"),
22306
+ offset: z28.number().default(0).describe("For enrich mode: skip this many eligible notes (for pagination)")
22761
22307
  },
22762
22308
  async ({ mode, dry_run, batch_size, offset }) => {
22763
22309
  const stateDb2 = getStateDb3();
@@ -22784,7 +22330,7 @@ function registerInitTools(server2, getVaultPath, getStateDb3) {
22784
22330
  }
22785
22331
 
22786
22332
  // src/tools/read/metrics.ts
22787
- import { z as z30 } from "zod";
22333
+ import { z as z29 } from "zod";
22788
22334
  function registerMetricsTools(server2, getIndex, getStateDb3) {
22789
22335
  server2.registerTool(
22790
22336
  "vault_growth",
@@ -22792,10 +22338,10 @@ function registerMetricsTools(server2, getIndex, getStateDb3) {
22792
22338
  title: "Vault Growth",
22793
22339
  description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago), "index_activity" (rebuild history). Tracks 11 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio, wikilink_accuracy, wikilink_feedback_volume, wikilink_suppressed_count.',
22794
22340
  inputSchema: {
22795
- mode: z30.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
22796
- metric: z30.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
22797
- days_back: z30.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
22798
- limit: z30.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
22341
+ mode: z29.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
22342
+ metric: z29.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
22343
+ days_back: z29.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
22344
+ limit: z29.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
22799
22345
  }
22800
22346
  },
22801
22347
  async ({ mode, metric, days_back, limit: eventLimit }) => {
@@ -22868,7 +22414,7 @@ function registerMetricsTools(server2, getIndex, getStateDb3) {
22868
22414
  }
22869
22415
 
22870
22416
  // src/tools/read/activity.ts
22871
- import { z as z31 } from "zod";
22417
+ import { z as z30 } from "zod";
22872
22418
  function registerActivityTools(server2, getStateDb3, getSessionId2) {
22873
22419
  server2.registerTool(
22874
22420
  "vault_activity",
@@ -22876,10 +22422,10 @@ function registerActivityTools(server2, getStateDb3, getSessionId2) {
22876
22422
  title: "Vault Activity",
22877
22423
  description: 'Track tool usage patterns and session activity. Modes:\n- "session": Current session summary (tools called, notes accessed)\n- "sessions": List of recent sessions\n- "note_access": Notes ranked by query frequency\n- "tool_usage": Tool usage patterns (most-used tools, avg duration)',
22878
22424
  inputSchema: {
22879
- mode: z31.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
22880
- session_id: z31.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
22881
- days_back: z31.number().optional().describe("Number of days to look back (default: 30)"),
22882
- limit: z31.number().optional().describe("Maximum results to return (default: 20)")
22425
+ mode: z30.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
22426
+ session_id: z30.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
22427
+ days_back: z30.number().optional().describe("Number of days to look back (default: 30)"),
22428
+ limit: z30.number().optional().describe("Maximum results to return (default: 20)")
22883
22429
  }
22884
22430
  },
22885
22431
  async ({ mode, session_id, days_back, limit: resultLimit }) => {
@@ -22946,12 +22492,65 @@ function registerActivityTools(server2, getStateDb3, getSessionId2) {
22946
22492
  }
22947
22493
 
22948
22494
  // src/tools/read/similarity.ts
22949
- import { z as z32 } from "zod";
22495
+ import { z as z31 } from "zod";
22950
22496
 
22951
22497
  // src/core/read/similarity.ts
22952
22498
  init_embeddings();
22953
22499
  import * as fs33 from "fs";
22954
22500
  import * as path36 from "path";
22501
+
22502
+ // src/core/read/mmr.ts
22503
+ init_embeddings();
22504
+ function selectByMmr(candidates, limit, lambda = 0.7) {
22505
+ if (candidates.length <= limit) return candidates;
22506
+ if (candidates.length === 0) return [];
22507
+ const maxScore = Math.max(...candidates.map((c) => c.score));
22508
+ if (maxScore === 0) return candidates.slice(0, limit);
22509
+ const normScores = /* @__PURE__ */ new Map();
22510
+ for (const c of candidates) {
22511
+ normScores.set(c.id, c.score / maxScore);
22512
+ }
22513
+ const selected = [];
22514
+ const remaining = new Set(candidates.map((_, i) => i));
22515
+ let bestIdx = 0;
22516
+ let bestScore = -Infinity;
22517
+ for (const idx of remaining) {
22518
+ if (candidates[idx].score > bestScore) {
22519
+ bestScore = candidates[idx].score;
22520
+ bestIdx = idx;
22521
+ }
22522
+ }
22523
+ selected.push(candidates[bestIdx]);
22524
+ remaining.delete(bestIdx);
22525
+ while (selected.length < limit && remaining.size > 0) {
22526
+ let bestMmr = -Infinity;
22527
+ let bestCandidate = -1;
22528
+ for (const idx of remaining) {
22529
+ const candidate = candidates[idx];
22530
+ const relevance = normScores.get(candidate.id) || 0;
22531
+ let maxSim = 0;
22532
+ if (candidate.embedding !== null) {
22533
+ for (const sel of selected) {
22534
+ if (sel.embedding !== null) {
22535
+ const sim = cosineSimilarity(candidate.embedding, sel.embedding);
22536
+ if (sim > maxSim) maxSim = sim;
22537
+ }
22538
+ }
22539
+ }
22540
+ const mmr = lambda * relevance - (1 - lambda) * maxSim;
22541
+ if (mmr > bestMmr) {
22542
+ bestMmr = mmr;
22543
+ bestCandidate = idx;
22544
+ }
22545
+ }
22546
+ if (bestCandidate === -1) break;
22547
+ selected.push(candidates[bestCandidate]);
22548
+ remaining.delete(bestCandidate);
22549
+ }
22550
+ return selected;
22551
+ }
22552
+
22553
+ // src/core/read/similarity.ts
22955
22554
  var STOP_WORDS = /* @__PURE__ */ new Set([
22956
22555
  "the",
22957
22556
  "be",
@@ -23225,9 +22824,9 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb3) {
23225
22824
  title: "Find Similar Notes",
23226
22825
  description: "Find notes similar to a given note using FTS5 keyword matching. When embeddings have been built (via init_semantic), automatically uses hybrid ranking (BM25 + embedding similarity via Reciprocal Rank Fusion). Already-linked notes are automatically excluded.",
23227
22826
  inputSchema: {
23228
- path: z32.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
23229
- limit: z32.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
23230
- diversity: z32.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
22827
+ path: z31.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
22828
+ limit: z31.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
22829
+ diversity: z31.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
23231
22830
  }
23232
22831
  },
23233
22832
  async ({ path: path39, limit, diversity }) => {
@@ -23272,7 +22871,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb3) {
23272
22871
 
23273
22872
  // src/tools/read/semantic.ts
23274
22873
  init_embeddings();
23275
- import { z as z33 } from "zod";
22874
+ import { z as z32 } from "zod";
23276
22875
  import { getAllEntitiesFromDb as getAllEntitiesFromDb3 } from "@velvetmonkey/vault-core";
23277
22876
  function registerSemanticTools(server2, getVaultPath, getStateDb3) {
23278
22877
  server2.registerTool(
@@ -23281,7 +22880,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb3) {
23281
22880
  title: "Initialize Semantic Search",
23282
22881
  description: "Download the embedding model and build semantic search index for this vault. After running, search and find_similar automatically use hybrid ranking (BM25 + semantic). Run once per vault \u2014 subsequent calls skip already-embedded notes unless force=true.",
23283
22882
  inputSchema: {
23284
- force: z33.boolean().optional().describe(
22883
+ force: z32.boolean().optional().describe(
23285
22884
  "Rebuild all embeddings even if they already exist (default: false)"
23286
22885
  )
23287
22886
  }
@@ -23376,7 +22975,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb3) {
23376
22975
 
23377
22976
  // src/tools/read/merges.ts
23378
22977
  init_levenshtein();
23379
- import { z as z34 } from "zod";
22978
+ import { z as z33 } from "zod";
23380
22979
  import { getAllEntitiesFromDb as getAllEntitiesFromDb4, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
23381
22980
  function normalizeName(name) {
23382
22981
  return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
@@ -23386,7 +22985,7 @@ function registerMergeTools2(server2, getStateDb3) {
23386
22985
  "suggest_entity_merges",
23387
22986
  "Find potential duplicate entities that could be merged based on name similarity",
23388
22987
  {
23389
- limit: z34.number().optional().default(50).describe("Maximum number of suggestions to return")
22988
+ limit: z33.number().optional().default(50).describe("Maximum number of suggestions to return")
23390
22989
  },
23391
22990
  async ({ limit }) => {
23392
22991
  const stateDb2 = getStateDb3();
@@ -23488,11 +23087,11 @@ function registerMergeTools2(server2, getStateDb3) {
23488
23087
  "dismiss_merge_suggestion",
23489
23088
  "Permanently dismiss a merge suggestion so it never reappears",
23490
23089
  {
23491
- source_path: z34.string().describe("Path of the source entity"),
23492
- target_path: z34.string().describe("Path of the target entity"),
23493
- source_name: z34.string().describe("Name of the source entity"),
23494
- target_name: z34.string().describe("Name of the target entity"),
23495
- reason: z34.string().describe("Original suggestion reason")
23090
+ source_path: z33.string().describe("Path of the source entity"),
23091
+ target_path: z33.string().describe("Path of the target entity"),
23092
+ source_name: z33.string().describe("Name of the source entity"),
23093
+ target_name: z33.string().describe("Name of the target entity"),
23094
+ reason: z33.string().describe("Original suggestion reason")
23496
23095
  },
23497
23096
  async ({ source_path, target_path, source_name, target_name, reason }) => {
23498
23097
  const stateDb2 = getStateDb3();
@@ -23511,7 +23110,7 @@ function registerMergeTools2(server2, getStateDb3) {
23511
23110
  }
23512
23111
 
23513
23112
  // src/tools/read/temporalAnalysis.ts
23514
- import { z as z35 } from "zod";
23113
+ import { z as z34 } from "zod";
23515
23114
  init_wikilinks();
23516
23115
  function formatDate3(d) {
23517
23116
  return d.toISOString().split("T")[0];
@@ -24037,9 +23636,9 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24037
23636
  title: "Context Around Date",
24038
23637
  description: "Reconstruct what was happening in the vault around a specific date. Shows modified/created notes, active entities, wikilink activity, and file moves within a time window.",
24039
23638
  inputSchema: {
24040
- date: z35.string().describe("Center date in YYYY-MM-DD format"),
24041
- window_days: z35.coerce.number().default(3).describe("Days before and after the center date (default 3 = 7-day window)"),
24042
- limit: z35.coerce.number().default(50).describe("Maximum number of notes to return")
23639
+ date: z34.string().describe("Center date in YYYY-MM-DD format"),
23640
+ window_days: z34.coerce.number().default(3).describe("Days before and after the center date (default 3 = 7-day window)"),
23641
+ limit: z34.coerce.number().default(50).describe("Maximum number of notes to return")
24043
23642
  }
24044
23643
  },
24045
23644
  async ({ date, window_days, limit: requestedLimit }) => {
@@ -24054,12 +23653,12 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24054
23653
  title: "Predict Stale Notes",
24055
23654
  description: "Multi-signal staleness prediction. Scores notes by importance (backlinks, hub score, tasks, status) and staleness risk (age, entity disconnect, task urgency). Returns concrete recommendations: archive, update, review, or low_priority.",
24056
23655
  inputSchema: {
24057
- days: z35.coerce.number().default(30).describe("Notes not modified in this many days (default 30)"),
24058
- min_importance: z35.coerce.number().default(0).describe("Filter by minimum importance score 0-100 (default 0)"),
24059
- include_recommendations: z35.boolean().default(true).describe("Include action recommendations (default true)"),
24060
- folder: z35.string().optional().describe("Limit to notes in this folder"),
24061
- limit: z35.coerce.number().default(30).describe("Maximum results to return (default 30)"),
24062
- offset: z35.coerce.number().default(0).describe("Results to skip for pagination (default 0)")
23656
+ days: z34.coerce.number().default(30).describe("Notes not modified in this many days (default 30)"),
23657
+ min_importance: z34.coerce.number().default(0).describe("Filter by minimum importance score 0-100 (default 0)"),
23658
+ include_recommendations: z34.boolean().default(true).describe("Include action recommendations (default true)"),
23659
+ folder: z34.string().optional().describe("Limit to notes in this folder"),
23660
+ limit: z34.coerce.number().default(30).describe("Maximum results to return (default 30)"),
23661
+ offset: z34.coerce.number().default(0).describe("Results to skip for pagination (default 0)")
24063
23662
  }
24064
23663
  },
24065
23664
  async ({ days, min_importance, include_recommendations, folder, limit: requestedLimit, offset }) => {
@@ -24083,9 +23682,9 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24083
23682
  title: "Track Concept Evolution",
24084
23683
  description: "Timeline of how an entity has evolved: link additions/removals, feedback events, category changes, co-occurrence shifts. Shows current state, chronological event history, link durability stats, and top co-occurrence neighbors.",
24085
23684
  inputSchema: {
24086
- entity: z35.string().describe("Entity name (case-insensitive)"),
24087
- days_back: z35.coerce.number().default(90).describe("How far back to look (default 90 days)"),
24088
- include_cooccurrence: z35.boolean().default(true).describe("Include co-occurrence neighbors (default true)")
23685
+ entity: z34.string().describe("Entity name (case-insensitive)"),
23686
+ days_back: z34.coerce.number().default(90).describe("How far back to look (default 90 days)"),
23687
+ include_cooccurrence: z34.boolean().default(true).describe("Include co-occurrence neighbors (default true)")
24089
23688
  }
24090
23689
  },
24091
23690
  async ({ entity, days_back, include_cooccurrence }) => {
@@ -24105,10 +23704,10 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24105
23704
  title: "Temporal Summary",
24106
23705
  description: "Generate a vault pulse report for a time period. Composes context, staleness prediction, and concept evolution into a single summary. Shows activity snapshot, entity momentum, and maintenance alerts. Use for weekly/monthly/quarterly reviews.",
24107
23706
  inputSchema: {
24108
- start_date: z35.string().describe("Start of period in YYYY-MM-DD format"),
24109
- end_date: z35.string().describe("End of period in YYYY-MM-DD format"),
24110
- focus_entities: z35.array(z35.string()).optional().describe("Specific entities to track evolution for (default: top 5 active entities in period)"),
24111
- limit: z35.coerce.number().default(50).describe("Maximum notes to include in context snapshot")
23707
+ start_date: z34.string().describe("Start of period in YYYY-MM-DD format"),
23708
+ end_date: z34.string().describe("End of period in YYYY-MM-DD format"),
23709
+ focus_entities: z34.array(z34.string()).optional().describe("Specific entities to track evolution for (default: top 5 active entities in period)"),
23710
+ limit: z34.coerce.number().default(50).describe("Maximum notes to include in context snapshot")
24112
23711
  }
24113
23712
  },
24114
23713
  async ({ start_date, end_date, focus_entities, limit: requestedLimit }) => {
@@ -24127,15 +23726,15 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24127
23726
  }
24128
23727
 
24129
23728
  // src/tools/read/sessionHistory.ts
24130
- import { z as z36 } from "zod";
23729
+ import { z as z35 } from "zod";
24131
23730
  function registerSessionHistoryTools(server2, getStateDb3) {
24132
23731
  server2.tool(
24133
23732
  "vault_session_history",
24134
23733
  "View session history. Without session_id: lists recent sessions. With session_id: returns chronological tool invocations, notes accessed, and timing. Hierarchical sessions supported (parent ID includes child sessions).",
24135
23734
  {
24136
- session_id: z36.string().optional().describe("Session ID for detail view. Omit for recent sessions list."),
24137
- include_children: z36.boolean().optional().describe("Include child sessions (default: true)"),
24138
- limit: z36.number().min(1).max(500).optional().describe("Max invocations to return in detail view (default: 200)")
23735
+ session_id: z35.string().optional().describe("Session ID for detail view. Omit for recent sessions list."),
23736
+ include_children: z35.boolean().optional().describe("Include child sessions (default: true)"),
23737
+ limit: z35.number().min(1).max(500).optional().describe("Max invocations to return in detail view (default: 200)")
24139
23738
  },
24140
23739
  async (args) => {
24141
23740
  const stateDb2 = getStateDb3();
@@ -24183,7 +23782,7 @@ function registerSessionHistoryTools(server2, getStateDb3) {
24183
23782
  }
24184
23783
 
24185
23784
  // src/tools/read/entityHistory.ts
24186
- import { z as z37 } from "zod";
23785
+ import { z as z36 } from "zod";
24187
23786
 
24188
23787
  // src/core/read/entityHistory.ts
24189
23788
  function normalizeTimestamp(ts) {
@@ -24341,8 +23940,8 @@ function registerEntityHistoryTools(server2, getStateDb3) {
24341
23940
  "vault_entity_history",
24342
23941
  "Get a unified timeline of everything about an entity: when it was linked, feedback received, suggestion scores, edge weight changes, metadata mutations, memories, and corrections. Sorted chronologically with pagination.",
24343
23942
  {
24344
- entity_name: z37.string().describe("Entity name to query (case-insensitive)"),
24345
- event_types: z37.array(z37.enum([
23943
+ entity_name: z36.string().describe("Entity name to query (case-insensitive)"),
23944
+ event_types: z36.array(z36.enum([
24346
23945
  "application",
24347
23946
  "feedback",
24348
23947
  "suggestion",
@@ -24351,10 +23950,10 @@ function registerEntityHistoryTools(server2, getStateDb3) {
24351
23950
  "memory",
24352
23951
  "correction"
24353
23952
  ])).optional().describe("Filter to specific event types. Omit for all types."),
24354
- start_date: z37.string().optional().describe("Start date (YYYY-MM-DD) for date range filter"),
24355
- end_date: z37.string().optional().describe("End date (YYYY-MM-DD) for date range filter"),
24356
- limit: z37.number().min(1).max(200).optional().describe("Max events to return (default: 50)"),
24357
- offset: z37.number().min(0).optional().describe("Offset for pagination (default: 0)")
23953
+ start_date: z36.string().optional().describe("Start date (YYYY-MM-DD) for date range filter"),
23954
+ end_date: z36.string().optional().describe("End date (YYYY-MM-DD) for date range filter"),
23955
+ limit: z36.number().min(1).max(200).optional().describe("Max events to return (default: 50)"),
23956
+ offset: z36.number().min(0).optional().describe("Offset for pagination (default: 0)")
24358
23957
  },
24359
23958
  async (args) => {
24360
23959
  const stateDb2 = getStateDb3();
@@ -24386,7 +23985,7 @@ function registerEntityHistoryTools(server2, getStateDb3) {
24386
23985
  }
24387
23986
 
24388
23987
  // src/tools/read/learningReport.ts
24389
- import { z as z38 } from "zod";
23988
+ import { z as z37 } from "zod";
24390
23989
 
24391
23990
  // src/core/read/learningReport.ts
24392
23991
  function isoDate(d) {
@@ -24522,8 +24121,8 @@ function registerLearningReportTools(server2, getIndex, getStateDb3) {
24522
24121
  "flywheel_learning_report",
24523
24122
  "Get a narrative report of the flywheel auto-linking system's learning progress. Shows: applications by day, feedback (positive/negative), survival rate, top rejected entities, suggestion funnel (evaluations \u2192 applications \u2192 survivals), and graph growth. Use compare=true for period-over-period deltas.",
24524
24123
  {
24525
- days_back: z38.number().min(1).max(365).optional().describe("Analysis window in days (default: 7). Use 1 for today, 2 for last 48h, etc."),
24526
- compare: z38.boolean().optional().describe("Include comparison with the preceding equal-length period (default: false)")
24124
+ days_back: z37.number().min(1).max(365).optional().describe("Analysis window in days (default: 7). Use 1 for today, 2 for last 48h, etc."),
24125
+ compare: z37.boolean().optional().describe("Include comparison with the preceding equal-length period (default: false)")
24527
24126
  },
24528
24127
  async (args) => {
24529
24128
  const stateDb2 = getStateDb3();
@@ -24550,7 +24149,7 @@ function registerLearningReportTools(server2, getIndex, getStateDb3) {
24550
24149
  }
24551
24150
 
24552
24151
  // src/tools/read/calibrationExport.ts
24553
- import { z as z39 } from "zod";
24152
+ import { z as z38 } from "zod";
24554
24153
 
24555
24154
  // src/core/read/calibrationExport.ts
24556
24155
  init_wikilinkFeedback();
@@ -24843,8 +24442,8 @@ function registerCalibrationExportTools(server2, getIndex, getStateDb3, getConfi
24843
24442
  "flywheel_calibration_export",
24844
24443
  "Export anonymized aggregate scoring data for cross-vault algorithm calibration. No entity names, note paths, or content \u2014 safe to share. Includes: suggestion funnel, per-layer contribution averages, survival rates by entity category, score distribution, suppression stats, recency/co-occurrence effectiveness, and threshold sweep.",
24845
24444
  {
24846
- days_back: z39.number().min(1).max(365).optional().describe("Analysis window in days (default: 30)"),
24847
- include_vault_id: z39.boolean().optional().describe("Include anonymous vault ID for longitudinal tracking (default: true)")
24445
+ days_back: z38.number().min(1).max(365).optional().describe("Analysis window in days (default: 30)"),
24446
+ include_vault_id: z38.boolean().optional().describe("Include anonymous vault ID for longitudinal tracking (default: true)")
24848
24447
  },
24849
24448
  async (args) => {
24850
24449
  const stateDb2 = getStateDb3();
@@ -25131,7 +24730,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
25131
24730
  const schemaIdx = handlerIdx - 1;
25132
24731
  const schema = args[schemaIdx];
25133
24732
  if (schema && typeof schema === "object" && !Array.isArray(schema)) {
25134
- schema.vault = z40.string().optional().describe(
24733
+ schema.vault = z39.string().optional().describe(
25135
24734
  `Vault name for multi-vault mode. Available: ${registry.getVaultNames().join(", ")}. Default: ${registry.primaryName}`
25136
24735
  );
25137
24736
  }
@@ -25257,7 +24856,6 @@ function registerAllTools(targetServer, ctx) {
25257
24856
  registerLearningReportTools(targetServer, gvi, gsd);
25258
24857
  registerCalibrationExportTools(targetServer, gvi, gsd, gcf);
25259
24858
  registerMemoryTools(targetServer, gsd);
25260
- registerRecallTools(targetServer, gsd, gvp, () => gvi() ?? null);
25261
24859
  registerBriefTools(targetServer, gsd);
25262
24860
  registerVaultResources(targetServer, () => gvi() ?? null);
25263
24861
  }
@@ -25441,7 +25039,10 @@ function updateFlywheelConfig(config) {
25441
25039
  flywheelConfig = config;
25442
25040
  setWikilinkConfig(config);
25443
25041
  const ctx = getActiveVaultContext();
25444
- if (ctx) ctx.flywheelConfig = config;
25042
+ if (ctx) {
25043
+ ctx.flywheelConfig = config;
25044
+ setActiveScope(buildVaultScope(ctx));
25045
+ }
25445
25046
  }
25446
25047
  async function bootVault(ctx, startTime) {
25447
25048
  const vp = ctx.vaultPath;
@@ -25594,6 +25195,7 @@ async function main() {
25594
25195
  loadVaultCooccurrence(primaryCtx);
25595
25196
  activateVault(primaryCtx);
25596
25197
  await bootVault(primaryCtx, startTime);
25198
+ activateVault(primaryCtx);
25597
25199
  if (vaultConfigs && vaultConfigs.length > 1) {
25598
25200
  const secondaryConfigs = vaultConfigs.slice(1);
25599
25201
  (async () => {
@@ -25622,7 +25224,8 @@ async function updateEntitiesInStateDb(vp, sd) {
25622
25224
  const config = loadConfig(db4);
25623
25225
  const excludeFolders = config.exclude_entity_folders?.length ? config.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
25624
25226
  const entityIndex2 = await scanVaultEntities4(vault, {
25625
- excludeFolders
25227
+ excludeFolders,
25228
+ customCategories: config.custom_categories
25626
25229
  });
25627
25230
  db4.replaceAllEntities(entityIndex2);
25628
25231
  serverLog("index", `Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.148",
3
+ "version": "2.0.150",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 74 tools for search, backlinks, graph queries, mutations, agent memory, and hybrid semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@huggingface/transformers": "^3.8.1",
56
56
  "@modelcontextprotocol/sdk": "^1.25.1",
57
- "@velvetmonkey/vault-core": "^2.0.148",
57
+ "@velvetmonkey/vault-core": "^2.0.150",
58
58
  "better-sqlite3": "^12.0.0",
59
59
  "chokidar": "^4.0.0",
60
60
  "gray-matter": "^4.0.3",
@@ -85,4 +85,4 @@
85
85
  "publishConfig": {
86
86
  "access": "public"
87
87
  }
88
- }
88
+ }