@velvetmonkey/flywheel-memory 2.0.149 → 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 +170 -572
  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;
@@ -4922,7 +4919,7 @@ function formatContent(content, format) {
4922
4919
  const lines = trimmed.split("\n");
4923
4920
  return lines.map((line, i) => {
4924
4921
  if (i === 0) return `- ${line}`;
4925
- if (line === "") return "";
4922
+ if (line === "") return " ";
4926
4923
  return ` ${line}`;
4927
4924
  }).join("\n");
4928
4925
  }
@@ -4933,7 +4930,7 @@ function formatContent(content, format) {
4933
4930
  const lines = trimmed.split("\n");
4934
4931
  return lines.map((line, i) => {
4935
4932
  if (i === 0) return `- [ ] ${line}`;
4936
- if (line === "") return "";
4933
+ if (line === "") return " ";
4937
4934
  return ` ${line}`;
4938
4935
  }).join("\n");
4939
4936
  }
@@ -4944,7 +4941,7 @@ function formatContent(content, format) {
4944
4941
  const lines = trimmed.split("\n");
4945
4942
  return lines.map((line, i) => {
4946
4943
  if (i === 0) return `1. ${line}`;
4947
- if (line === "") return "";
4944
+ if (line === "") return " ";
4948
4945
  return ` ${line}`;
4949
4946
  }).join("\n");
4950
4947
  }
@@ -4960,7 +4957,7 @@ function formatContent(content, format) {
4960
4957
  const indent = " ";
4961
4958
  return lines.map((line, i) => {
4962
4959
  if (i === 0) return `${prefix}${line}`;
4963
- if (line === "") return "";
4960
+ if (line === "") return indent;
4964
4961
  return `${indent}${line}`;
4965
4962
  }).join("\n");
4966
4963
  }
@@ -5037,7 +5034,7 @@ function insertInSection(content, section, newContent, position, options) {
5037
5034
  if (indent) {
5038
5035
  const contentLines = formattedContent.split("\n");
5039
5036
  const indentedContent = contentLines.map((line) => {
5040
- if (line === "") return line;
5037
+ if (line === "") return indent || line;
5041
5038
  return indent + line;
5042
5039
  }).join("\n");
5043
5040
  lines.splice(section.contentStartLine, 0, indentedContent);
@@ -5060,7 +5057,7 @@ function insertInSection(content, section, newContent, position, options) {
5060
5057
  const indent = detectSectionBaseIndentation(lines, section.contentStartLine, section.endLine);
5061
5058
  const contentLines = formattedContent.split("\n");
5062
5059
  const indentedContent = contentLines.map((line) => {
5063
- if (line === "") return line;
5060
+ if (line === "") return indent || line;
5064
5061
  return indent + line;
5065
5062
  }).join("\n");
5066
5063
  lines[lastContentLineIdx] = indentedContent;
@@ -5083,7 +5080,7 @@ function insertInSection(content, section, newContent, position, options) {
5083
5080
  const indent = detectSectionBaseIndentation(lines, section.contentStartLine, section.endLine);
5084
5081
  const contentLines = formattedContent.split("\n");
5085
5082
  const indentedContent = contentLines.map((line) => {
5086
- if (line === "") return line;
5083
+ if (line === "") return indent || line;
5087
5084
  return indent + line;
5088
5085
  }).join("\n");
5089
5086
  lines.splice(insertLine, 0, indentedContent);
@@ -10719,9 +10716,8 @@ var ALL_CATEGORIES = [
10719
10716
  ];
10720
10717
  var PRESETS = {
10721
10718
  // Presets
10722
- default: ["search", "read", "write", "tasks"],
10723
- agent: ["search", "read", "write", "memory"],
10724
- full: ALL_CATEGORIES.filter((c) => c !== "memory"),
10719
+ default: ["search", "read", "write", "tasks", "memory"],
10720
+ full: [...ALL_CATEGORIES],
10725
10721
  // Composable bundles (one per category)
10726
10722
  graph: ["graph"],
10727
10723
  schema: ["schema"],
@@ -10735,6 +10731,8 @@ var PRESETS = {
10735
10731
  };
10736
10732
  var DEFAULT_PRESET = "default";
10737
10733
  var DEPRECATED_ALIASES = {
10734
+ agent: "default",
10735
+ // agent merged into default — memory now included
10738
10736
  minimal: "default",
10739
10737
  writer: "default",
10740
10738
  // writer was default+tasks, now default includes tasks
@@ -10849,9 +10847,8 @@ var TOOL_CATEGORY = {
10849
10847
  tasks: "tasks",
10850
10848
  vault_toggle_task: "tasks",
10851
10849
  vault_add_task: "tasks",
10852
- // memory (3 tools) -- agent working memory
10850
+ // memory (2 tools) -- session memory
10853
10851
  memory: "memory",
10854
- recall: "memory",
10855
10852
  brief: "memory",
10856
10853
  // note-ops (4 tools) -- file management
10857
10854
  vault_delete_note: "note-ops",
@@ -10890,10 +10887,12 @@ function generateInstructions(categories, registry) {
10890
10887
  parts.push(`Flywheel provides tools to search, read, and write an Obsidian vault's knowledge graph.
10891
10888
 
10892
10889
  Tool selection:
10893
- 1. "search" is the primary tool. Each result includes: frontmatter, tags, aliases,
10894
- backlinks (with line numbers), outlinks (with line numbers and existence check),
10895
- headings, content snippet or preview, entity category, hub score, and timestamps.
10896
- 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.
10897
10896
  2. Escalate to "get_note_structure" only when you need the full markdown content
10898
10897
  or word count. Use "get_section_content" to read one section by heading name.
10899
10898
  3. Start with a broad search: just query text, no filters. Only add folder, tag,
@@ -10901,7 +10900,7 @@ Tool selection:
10901
10900
  if (!hasEmbeddingsIndex()) {
10902
10901
  parts.push(`
10903
10902
  **Setup:** Run \`init_semantic\` once to build embeddings. This unlocks hybrid search (BM25 + semantic),
10904
- 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.`);
10905
10904
  }
10906
10905
  if (registry?.isMultiVault) {
10907
10906
  parts.push(`
@@ -10916,9 +10915,9 @@ This server manages multiple vaults. Every tool has an optional "vault" paramete
10916
10915
  **Frontmatter matters more than content** for Flywheel's intelligence. When creating or updating notes, always set:
10917
10916
  - \`type:\` \u2014 drives entity categorization (person, project, technology). Without it, the category is guessed from the name alone and is often wrong.
10918
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.
10919
- - \`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.
10920
10919
  - Tags \u2014 used for filtering, suggestion scoring, and schema analysis.
10921
- 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.`);
10922
10921
  if (categories.has("read")) {
10923
10922
  parts.push(`
10924
10923
  ## Read
@@ -10969,7 +10968,10 @@ you say "run the weekly review for this week".`);
10969
10968
  parts.push(`
10970
10969
  ## Memory
10971
10970
 
10972
- 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).`);
10973
10975
  }
10974
10976
  if (categories.has("graph")) {
10975
10977
  parts.push(`
@@ -11014,7 +11016,7 @@ import * as path37 from "path";
11014
11016
  import { dirname as dirname5, join as join19 } from "path";
11015
11017
  import { statSync as statSync6, readFileSync as readFileSync5 } from "fs";
11016
11018
  import { fileURLToPath } from "url";
11017
- import { z as z40 } from "zod";
11019
+ import { z as z39 } from "zod";
11018
11020
  import { getSessionId } from "@velvetmonkey/vault-core";
11019
11021
  init_vault_scope();
11020
11022
 
@@ -13852,64 +13854,6 @@ function enrichResultLight(result, index, stateDb2) {
13852
13854
  }
13853
13855
  return enriched;
13854
13856
  }
13855
- function enrichEntityCompact(entityName, stateDb2, index) {
13856
- const enriched = {};
13857
- if (stateDb2) {
13858
- try {
13859
- const entity = getEntityByName2(stateDb2, entityName);
13860
- if (entity) {
13861
- enriched.category = entity.category;
13862
- enriched.hub_score = entity.hubScore;
13863
- if (entity.aliases.length > 0) enriched.aliases = entity.aliases;
13864
- enriched.path = entity.path;
13865
- }
13866
- } catch {
13867
- }
13868
- }
13869
- if (index) {
13870
- const entityPath = enriched.path ?? index.entities.get(entityName.toLowerCase());
13871
- if (entityPath) {
13872
- const note = index.notes.get(entityPath);
13873
- const normalizedPath = entityPath.toLowerCase().replace(/\.md$/, "");
13874
- const backlinks = index.backlinks.get(normalizedPath) || [];
13875
- enriched.backlink_count = backlinks.length;
13876
- if (note) {
13877
- if (Object.keys(note.frontmatter).length > 0) enriched.frontmatter = note.frontmatter;
13878
- if (note.tags.length > 0) enriched.tags = note.tags;
13879
- if (note.outlinks.length > 0) {
13880
- enriched.outlink_names = getOutlinkNames(note.outlinks, entityPath, index, stateDb2, COMPACT_OUTLINK_NAMES);
13881
- }
13882
- }
13883
- }
13884
- }
13885
- return enriched;
13886
- }
13887
- function enrichNoteCompact(notePath, stateDb2, index) {
13888
- const enriched = {};
13889
- if (!index) return enriched;
13890
- const note = index.notes.get(notePath);
13891
- if (!note) return enriched;
13892
- const normalizedPath = notePath.toLowerCase().replace(/\.md$/, "");
13893
- const backlinks = index.backlinks.get(normalizedPath) || [];
13894
- if (Object.keys(note.frontmatter).length > 0) enriched.frontmatter = note.frontmatter;
13895
- if (note.tags.length > 0) enriched.tags = note.tags;
13896
- enriched.backlink_count = backlinks.length;
13897
- enriched.modified = note.modified.toISOString();
13898
- if (note.outlinks.length > 0) {
13899
- enriched.outlink_names = getOutlinkNames(note.outlinks, notePath, index, stateDb2, COMPACT_OUTLINK_NAMES);
13900
- }
13901
- if (stateDb2) {
13902
- try {
13903
- const entity = getEntityByName2(stateDb2, note.title);
13904
- if (entity) {
13905
- enriched.category = entity.category;
13906
- enriched.hub_score = entity.hubScore;
13907
- }
13908
- } catch {
13909
- }
13910
- }
13911
- return enriched;
13912
- }
13913
13857
 
13914
13858
  // src/core/read/multihop.ts
13915
13859
  import { getEntityByName as getEntityByName3, searchEntities } from "@velvetmonkey/vault-core";
@@ -14382,7 +14326,7 @@ function sortNotes(notes, sortBy, order) {
14382
14326
  function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
14383
14327
  server2.tool(
14384
14328
  "search",
14385
- '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.',
14386
14330
  {
14387
14331
  query: z5.string().optional().describe("Search query text. Required unless using metadata filters (where, has_tag, folder, etc.)"),
14388
14332
  // Metadata filters
@@ -21771,410 +21715,8 @@ function registerMemoryTools(server2, getStateDb3) {
21771
21715
  );
21772
21716
  }
21773
21717
 
21774
- // src/tools/read/recall.ts
21775
- import { z as z26 } from "zod";
21776
- import { searchEntities as searchEntitiesDb2 } from "@velvetmonkey/vault-core";
21777
- init_recency();
21778
- init_cooccurrence();
21779
- init_wikilinks();
21780
- init_edgeWeights();
21781
- init_wikilinkFeedback();
21782
- init_embeddings();
21783
- init_stemmer();
21784
-
21785
- // src/core/read/mmr.ts
21786
- init_embeddings();
21787
- function selectByMmr(candidates, limit, lambda = 0.7) {
21788
- if (candidates.length <= limit) return candidates;
21789
- if (candidates.length === 0) return [];
21790
- const maxScore = Math.max(...candidates.map((c) => c.score));
21791
- if (maxScore === 0) return candidates.slice(0, limit);
21792
- const normScores = /* @__PURE__ */ new Map();
21793
- for (const c of candidates) {
21794
- normScores.set(c.id, c.score / maxScore);
21795
- }
21796
- const selected = [];
21797
- const remaining = new Set(candidates.map((_, i) => i));
21798
- let bestIdx = 0;
21799
- let bestScore = -Infinity;
21800
- for (const idx of remaining) {
21801
- if (candidates[idx].score > bestScore) {
21802
- bestScore = candidates[idx].score;
21803
- bestIdx = idx;
21804
- }
21805
- }
21806
- selected.push(candidates[bestIdx]);
21807
- remaining.delete(bestIdx);
21808
- while (selected.length < limit && remaining.size > 0) {
21809
- let bestMmr = -Infinity;
21810
- let bestCandidate = -1;
21811
- for (const idx of remaining) {
21812
- const candidate = candidates[idx];
21813
- const relevance = normScores.get(candidate.id) || 0;
21814
- let maxSim = 0;
21815
- if (candidate.embedding !== null) {
21816
- for (const sel of selected) {
21817
- if (sel.embedding !== null) {
21818
- const sim = cosineSimilarity(candidate.embedding, sel.embedding);
21819
- if (sim > maxSim) maxSim = sim;
21820
- }
21821
- }
21822
- }
21823
- const mmr = lambda * relevance - (1 - lambda) * maxSim;
21824
- if (mmr > bestMmr) {
21825
- bestMmr = mmr;
21826
- bestCandidate = idx;
21827
- }
21828
- }
21829
- if (bestCandidate === -1) break;
21830
- selected.push(candidates[bestCandidate]);
21831
- remaining.delete(bestCandidate);
21832
- }
21833
- return selected;
21834
- }
21835
-
21836
- // src/tools/read/recall.ts
21837
- function scoreTextRelevance(query, content) {
21838
- const queryTokens = tokenize(query).map((t) => t.toLowerCase());
21839
- const queryStems = queryTokens.map((t) => stem(t));
21840
- const contentLower = content.toLowerCase();
21841
- const contentTokens = new Set(tokenize(contentLower));
21842
- const contentStems = new Set([...contentTokens].map((t) => stem(t)));
21843
- let score = 0;
21844
- for (let i = 0; i < queryTokens.length; i++) {
21845
- const token = queryTokens[i];
21846
- const stemmed = queryStems[i];
21847
- if (contentTokens.has(token)) {
21848
- score += 10;
21849
- } else if (contentStems.has(stemmed)) {
21850
- score += 5;
21851
- }
21852
- }
21853
- if (contentLower.includes(query.toLowerCase())) {
21854
- score += 15;
21855
- }
21856
- return score;
21857
- }
21858
- function getEdgeWeightBoost(entityName, edgeWeightMap) {
21859
- const avgWeight = edgeWeightMap.get(entityName.toLowerCase());
21860
- if (!avgWeight || avgWeight <= 1) return 0;
21861
- return Math.min((avgWeight - 1) * 3, 6);
21862
- }
21863
- async function performRecall(stateDb2, query, options = {}) {
21864
- const {
21865
- max_results = 20,
21866
- focus,
21867
- entity,
21868
- max_tokens,
21869
- diversity = 1,
21870
- vaultPath: vaultPath2
21871
- } = options;
21872
- const results = [];
21873
- const recencyIndex2 = loadRecencyFromStateDb();
21874
- const edgeWeightMap = getEntityEdgeWeightMap(stateDb2);
21875
- const feedbackBoosts = getAllFeedbackBoosts(stateDb2);
21876
- const cooccurrenceIndex2 = getCooccurrenceIndex();
21877
- if (!focus || focus === "entities") {
21878
- try {
21879
- const entityResults = searchEntitiesDb2(stateDb2, query, max_results);
21880
- for (const e of entityResults) {
21881
- const textScore = scoreTextRelevance(query, `${e.name} ${e.description || ""}`);
21882
- const recency = recencyIndex2 ? getRecencyBoost(e.name, recencyIndex2) : 0;
21883
- const feedback = feedbackBoosts.get(e.name) ?? 0;
21884
- const edgeWeight = getEdgeWeightBoost(e.name, edgeWeightMap);
21885
- const total = textScore + recency + feedback + edgeWeight;
21886
- if (total > 0) {
21887
- results.push({
21888
- type: "entity",
21889
- id: e.name,
21890
- content: e.description || `Entity: ${e.name} (${e.category})`,
21891
- score: total,
21892
- breakdown: {
21893
- textRelevance: textScore,
21894
- recencyBoost: recency,
21895
- cooccurrenceBoost: 0,
21896
- feedbackBoost: feedback,
21897
- edgeWeightBoost: edgeWeight,
21898
- semanticBoost: 0
21899
- }
21900
- });
21901
- }
21902
- }
21903
- } catch {
21904
- }
21905
- }
21906
- if (!focus || focus === "notes") {
21907
- try {
21908
- const noteResults = searchFTS5("", query, max_results);
21909
- for (const n of noteResults) {
21910
- const textScore = Math.max(10, scoreTextRelevance(query, `${n.title || ""} ${n.snippet || ""}`));
21911
- const entityName = n.title || n.path.replace(/\.md$/, "").split("/").pop() || "";
21912
- const recency = recencyIndex2 ? getRecencyBoost(entityName, recencyIndex2) : 0;
21913
- const feedback = feedbackBoosts.get(entityName) ?? 0;
21914
- const edgeWeight = getEdgeWeightBoost(entityName, edgeWeightMap);
21915
- const total = textScore + recency + feedback + edgeWeight;
21916
- results.push({
21917
- type: "note",
21918
- id: n.path,
21919
- content: n.snippet || n.title || n.path,
21920
- score: total,
21921
- breakdown: {
21922
- textRelevance: textScore,
21923
- recencyBoost: recency,
21924
- cooccurrenceBoost: 0,
21925
- // applied in post-pass below
21926
- feedbackBoost: feedback,
21927
- edgeWeightBoost: edgeWeight,
21928
- semanticBoost: 0
21929
- }
21930
- });
21931
- }
21932
- } catch {
21933
- }
21934
- }
21935
- if (!focus || focus === "memories") {
21936
- try {
21937
- const memResults = searchMemories(stateDb2, {
21938
- query,
21939
- entity,
21940
- limit: max_results
21941
- });
21942
- const now = Date.now();
21943
- for (const m of memResults) {
21944
- const textScore = scoreTextRelevance(query, `${m.key} ${m.value}`);
21945
- const confidenceBoost = m.confidence * 5;
21946
- let typeBoost = 0;
21947
- switch (m.memory_type) {
21948
- case "fact":
21949
- typeBoost = 3;
21950
- break;
21951
- case "preference":
21952
- typeBoost = 2;
21953
- break;
21954
- case "observation": {
21955
- const ageDays = (now - m.updated_at) / 864e5;
21956
- const recencyFactor = Math.max(0.2, 1 - ageDays / 7);
21957
- typeBoost = 1 + 4 * recencyFactor;
21958
- break;
21959
- }
21960
- case "summary":
21961
- typeBoost = 1;
21962
- break;
21963
- }
21964
- const memScore = textScore + confidenceBoost + typeBoost;
21965
- results.push({
21966
- type: "memory",
21967
- id: m.key,
21968
- content: m.value,
21969
- score: memScore,
21970
- breakdown: {
21971
- textRelevance: textScore,
21972
- recencyBoost: typeBoost,
21973
- // type-aware boost reported as recency
21974
- cooccurrenceBoost: 0,
21975
- feedbackBoost: confidenceBoost,
21976
- edgeWeightBoost: 0,
21977
- semanticBoost: 0
21978
- }
21979
- });
21980
- }
21981
- } catch {
21982
- }
21983
- }
21984
- if (cooccurrenceIndex2) {
21985
- const seedEntities = /* @__PURE__ */ new Set();
21986
- for (const r of results) {
21987
- if (r.type === "entity") {
21988
- seedEntities.add(r.id);
21989
- } else if (r.type === "note") {
21990
- const name = r.id.replace(/\.md$/, "").split("/").pop() || "";
21991
- if (name) seedEntities.add(name);
21992
- }
21993
- }
21994
- for (const r of results) {
21995
- if (r.type === "entity" || r.type === "note") {
21996
- const name = r.type === "entity" ? r.id : r.id.replace(/\.md$/, "").split("/").pop() || "";
21997
- const boost = getCooccurrenceBoost(name, seedEntities, cooccurrenceIndex2, recencyIndex2);
21998
- if (boost > 0) {
21999
- r.breakdown.cooccurrenceBoost = boost;
22000
- r.score += boost;
22001
- }
22002
- }
22003
- }
22004
- }
22005
- if ((!focus || focus === "entities") && query.length >= 20 && hasEntityEmbeddingsIndex()) {
22006
- try {
22007
- const embedding = await embedTextCached(query);
22008
- const semanticMatches = findSemanticallySimilarEntities(embedding, max_results);
22009
- for (const match of semanticMatches) {
22010
- if (match.similarity < 0.3) continue;
22011
- const boost = match.similarity * 15;
22012
- const existing = results.find((r) => r.type === "entity" && r.id === match.entityName);
22013
- if (existing) {
22014
- existing.score += boost;
22015
- existing.breakdown.semanticBoost = boost;
22016
- } else {
22017
- results.push({
22018
- type: "entity",
22019
- id: match.entityName,
22020
- content: `Semantically similar to: "${query}"`,
22021
- score: boost,
22022
- breakdown: {
22023
- textRelevance: 0,
22024
- recencyBoost: 0,
22025
- cooccurrenceBoost: 0,
22026
- feedbackBoost: 0,
22027
- edgeWeightBoost: 0,
22028
- semanticBoost: boost
22029
- }
22030
- });
22031
- }
22032
- }
22033
- } catch {
22034
- }
22035
- }
22036
- results.sort((a, b) => b.score - a.score);
22037
- const seen = /* @__PURE__ */ new Set();
22038
- const deduped = results.filter((r) => {
22039
- const key = `${r.type}:${r.id}`;
22040
- if (seen.has(key)) return false;
22041
- seen.add(key);
22042
- return true;
22043
- });
22044
- let selected;
22045
- if (hasEmbeddingsIndex() && deduped.length > max_results) {
22046
- const notePaths = deduped.filter((r) => r.type === "note").map((r) => r.id);
22047
- const noteEmbeddings = loadNoteEmbeddingsForPaths(notePaths);
22048
- let queryEmbedding = null;
22049
- const mmrCandidates = [];
22050
- for (const r of deduped) {
22051
- let embedding = null;
22052
- if (r.type === "entity") {
22053
- embedding = getEntityEmbedding(r.id);
22054
- } else if (r.type === "note") {
22055
- embedding = noteEmbeddings.get(r.id) ?? null;
22056
- } else if (r.type === "memory") {
22057
- try {
22058
- if (!queryEmbedding) queryEmbedding = await embedTextCached(query);
22059
- embedding = await embedTextCached(r.content);
22060
- } catch {
22061
- }
22062
- }
22063
- mmrCandidates.push({ id: `${r.type}:${r.id}`, score: r.score, embedding });
22064
- }
22065
- const mmrSelected = selectByMmr(mmrCandidates, max_results, diversity);
22066
- const selectedIds = new Set(mmrSelected.map((m) => m.id));
22067
- selected = deduped.filter((r) => selectedIds.has(`${r.type}:${r.id}`));
22068
- const orderMap = new Map(mmrSelected.map((m, i) => [m.id, i]));
22069
- selected.sort((a, b) => (orderMap.get(`${a.type}:${a.id}`) ?? 0) - (orderMap.get(`${b.type}:${b.id}`) ?? 0));
22070
- } else {
22071
- selected = deduped.slice(0, max_results);
22072
- }
22073
- if (vaultPath2) {
22074
- const queryTokens = tokenize(query).map((t) => t.toLowerCase());
22075
- let queryEmb = null;
22076
- if (hasEmbeddingsIndex()) {
22077
- try {
22078
- queryEmb = await embedTextCached(query);
22079
- } catch {
22080
- }
22081
- }
22082
- for (const r of selected) {
22083
- if (r.type !== "note") continue;
22084
- try {
22085
- const absPath = vaultPath2 + "/" + r.id;
22086
- const snippets = await extractBestSnippets(absPath, queryEmb, queryTokens);
22087
- if (snippets.length > 0 && snippets[0].text.length > 0) {
22088
- r.content = snippets[0].text;
22089
- }
22090
- } catch {
22091
- }
22092
- }
22093
- }
22094
- if (max_tokens) {
22095
- let tokenBudget = max_tokens;
22096
- const budgeted = [];
22097
- for (const r of selected) {
22098
- const estimatedTokens = Math.ceil(r.content.length / 4);
22099
- if (tokenBudget - estimatedTokens < 0 && budgeted.length > 0) break;
22100
- tokenBudget -= estimatedTokens;
22101
- budgeted.push(r);
22102
- }
22103
- return budgeted;
22104
- }
22105
- return selected;
22106
- }
22107
- function registerRecallTools(server2, getStateDb3, getVaultPath, getIndex) {
22108
- server2.tool(
22109
- "recall",
22110
- "Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
22111
- {
22112
- query: z26.string().describe('What to recall (e.g., "Project X", "meetings about auth")'),
22113
- max_results: z26.number().min(1).max(100).optional().describe("Max results (default: 20)"),
22114
- focus: z26.enum(["entities", "notes", "memories"]).optional().describe("Limit to a specific result type. Omit for best results (searches everything)."),
22115
- entity: z26.string().optional().describe("Filter memories by entity association"),
22116
- max_tokens: z26.number().optional().describe("Token budget for response (truncates lower-ranked results)"),
22117
- diversity: z26.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
22118
- },
22119
- async (args) => {
22120
- const stateDb2 = getStateDb3();
22121
- if (!stateDb2) {
22122
- return {
22123
- content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
22124
- isError: true
22125
- };
22126
- }
22127
- const results = await performRecall(stateDb2, args.query, {
22128
- max_results: args.max_results,
22129
- focus: args.focus,
22130
- entity: args.entity,
22131
- max_tokens: args.max_tokens,
22132
- diversity: args.diversity,
22133
- vaultPath: getVaultPath?.()
22134
- });
22135
- const entities = results.filter((r) => r.type === "entity");
22136
- const notes = results.filter((r) => r.type === "note");
22137
- const memories = results.filter((r) => r.type === "memory");
22138
- const index = getIndex?.() ?? null;
22139
- const enrichedNotes = notes.map((n) => ({
22140
- path: n.id,
22141
- snippet: n.content,
22142
- ...enrichNoteCompact(n.id, stateDb2, index)
22143
- }));
22144
- const hopResults = index ? multiHopBackfill(enrichedNotes, index, stateDb2, {
22145
- maxBackfill: Math.max(5, (args.max_results ?? 10) - notes.length)
22146
- }) : [];
22147
- if (index) {
22148
- const allNoteResults = [...enrichedNotes, ...hopResults];
22149
- const expansionTerms = extractExpansionTerms(allNoteResults, args.query, index);
22150
- const expansionResults = expandQuery(expansionTerms, allNoteResults, index, stateDb2);
22151
- hopResults.push(...expansionResults);
22152
- }
22153
- return {
22154
- content: [{
22155
- type: "text",
22156
- text: JSON.stringify({
22157
- query: args.query,
22158
- total: results.length,
22159
- entities: entities.map((e) => ({
22160
- name: e.id,
22161
- description: e.content,
22162
- ...enrichEntityCompact(e.id, stateDb2, index)
22163
- })),
22164
- notes: [...enrichedNotes, ...hopResults],
22165
- memories: memories.map((m) => ({
22166
- key: m.id,
22167
- value: m.content
22168
- }))
22169
- }, null, 2)
22170
- }]
22171
- };
22172
- }
22173
- );
22174
- }
22175
-
22176
21718
  // src/tools/read/brief.ts
22177
- import { z as z27 } from "zod";
21719
+ import { z as z26 } from "zod";
22178
21720
  init_corrections();
22179
21721
  function estimateTokens2(value) {
22180
21722
  const str = JSON.stringify(value);
@@ -22316,8 +21858,8 @@ function registerBriefTools(server2, getStateDb3) {
22316
21858
  "brief",
22317
21859
  "Get a startup context briefing: recent sessions, active entities, memories, pending corrections, and vault stats. Call at conversation start.",
22318
21860
  {
22319
- max_tokens: z27.number().optional().describe("Token budget (lower-priority sections truncated first)"),
22320
- 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)")
22321
21863
  },
22322
21864
  async (args) => {
22323
21865
  const stateDb2 = getStateDb3();
@@ -22368,24 +21910,24 @@ function registerBriefTools(server2, getStateDb3) {
22368
21910
  }
22369
21911
 
22370
21912
  // src/tools/write/config.ts
22371
- import { z as z28 } from "zod";
21913
+ import { z as z27 } from "zod";
22372
21914
  import { saveFlywheelConfigToDb as saveFlywheelConfigToDb2 } from "@velvetmonkey/vault-core";
22373
21915
  var VALID_CONFIG_KEYS = {
22374
- vault_name: z28.string(),
22375
- exclude_task_tags: z28.array(z28.string()),
22376
- exclude_analysis_tags: z28.array(z28.string()),
22377
- exclude_entities: z28.array(z28.string()),
22378
- exclude_entity_folders: z28.array(z28.string()),
22379
- wikilink_strictness: z28.enum(["conservative", "balanced", "aggressive"]),
22380
- implicit_detection: z28.boolean(),
22381
- implicit_patterns: z28.array(z28.string()),
22382
- adaptive_strictness: z28.boolean(),
22383
- proactive_linking: z28.boolean(),
22384
- proactive_min_score: z28.number(),
22385
- proactive_max_per_file: z28.number(),
22386
- proactive_max_per_day: z28.number(),
22387
- custom_categories: z28.record(z28.string(), z28.object({
22388
- 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()
22389
21931
  }))
22390
21932
  };
22391
21933
  function registerConfigTools(server2, getConfig2, setConfig, getStateDb3) {
@@ -22395,9 +21937,9 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb3) {
22395
21937
  title: "Flywheel Config",
22396
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"] })',
22397
21939
  inputSchema: {
22398
- mode: z28.enum(["get", "set"]).describe("Operation mode"),
22399
- key: z28.string().optional().describe("Config key to update (required for set mode)"),
22400
- 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)")
22401
21943
  }
22402
21944
  },
22403
21945
  async ({ mode, key, value }) => {
@@ -22453,7 +21995,7 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb3) {
22453
21995
  // src/tools/write/enrich.ts
22454
21996
  init_wikilinks();
22455
21997
  init_wikilinkFeedback();
22456
- import { z as z29 } from "zod";
21998
+ import { z as z28 } from "zod";
22457
21999
  import * as fs32 from "fs/promises";
22458
22000
  import * as path35 from "path";
22459
22001
  import { scanVaultEntities as scanVaultEntities3, SCHEMA_VERSION as SCHEMA_VERSION2 } from "@velvetmonkey/vault-core";
@@ -22758,10 +22300,10 @@ function registerInitTools(server2, getVaultPath, getStateDb3) {
22758
22300
  "vault_init",
22759
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).`,
22760
22302
  {
22761
- mode: z29.enum(["status", "run", "enrich"]).default("status").describe("Operation mode (default: status)"),
22762
- dry_run: z29.boolean().default(true).describe("For enrich mode: preview without modifying files (default: true)"),
22763
- batch_size: z29.number().default(50).describe("For enrich mode: max notes per invocation (default: 50)"),
22764
- 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)")
22765
22307
  },
22766
22308
  async ({ mode, dry_run, batch_size, offset }) => {
22767
22309
  const stateDb2 = getStateDb3();
@@ -22788,7 +22330,7 @@ function registerInitTools(server2, getVaultPath, getStateDb3) {
22788
22330
  }
22789
22331
 
22790
22332
  // src/tools/read/metrics.ts
22791
- import { z as z30 } from "zod";
22333
+ import { z as z29 } from "zod";
22792
22334
  function registerMetricsTools(server2, getIndex, getStateDb3) {
22793
22335
  server2.registerTool(
22794
22336
  "vault_growth",
@@ -22796,10 +22338,10 @@ function registerMetricsTools(server2, getIndex, getStateDb3) {
22796
22338
  title: "Vault Growth",
22797
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.',
22798
22340
  inputSchema: {
22799
- mode: z30.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
22800
- metric: z30.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
22801
- days_back: z30.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
22802
- 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)")
22803
22345
  }
22804
22346
  },
22805
22347
  async ({ mode, metric, days_back, limit: eventLimit }) => {
@@ -22872,7 +22414,7 @@ function registerMetricsTools(server2, getIndex, getStateDb3) {
22872
22414
  }
22873
22415
 
22874
22416
  // src/tools/read/activity.ts
22875
- import { z as z31 } from "zod";
22417
+ import { z as z30 } from "zod";
22876
22418
  function registerActivityTools(server2, getStateDb3, getSessionId2) {
22877
22419
  server2.registerTool(
22878
22420
  "vault_activity",
@@ -22880,10 +22422,10 @@ function registerActivityTools(server2, getStateDb3, getSessionId2) {
22880
22422
  title: "Vault Activity",
22881
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)',
22882
22424
  inputSchema: {
22883
- mode: z31.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
22884
- session_id: z31.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
22885
- days_back: z31.number().optional().describe("Number of days to look back (default: 30)"),
22886
- 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)")
22887
22429
  }
22888
22430
  },
22889
22431
  async ({ mode, session_id, days_back, limit: resultLimit }) => {
@@ -22950,12 +22492,65 @@ function registerActivityTools(server2, getStateDb3, getSessionId2) {
22950
22492
  }
22951
22493
 
22952
22494
  // src/tools/read/similarity.ts
22953
- import { z as z32 } from "zod";
22495
+ import { z as z31 } from "zod";
22954
22496
 
22955
22497
  // src/core/read/similarity.ts
22956
22498
  init_embeddings();
22957
22499
  import * as fs33 from "fs";
22958
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
22959
22554
  var STOP_WORDS = /* @__PURE__ */ new Set([
22960
22555
  "the",
22961
22556
  "be",
@@ -23229,9 +22824,9 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb3) {
23229
22824
  title: "Find Similar Notes",
23230
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.",
23231
22826
  inputSchema: {
23232
- path: z32.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
23233
- limit: z32.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
23234
- 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)")
23235
22830
  }
23236
22831
  },
23237
22832
  async ({ path: path39, limit, diversity }) => {
@@ -23276,7 +22871,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb3) {
23276
22871
 
23277
22872
  // src/tools/read/semantic.ts
23278
22873
  init_embeddings();
23279
- import { z as z33 } from "zod";
22874
+ import { z as z32 } from "zod";
23280
22875
  import { getAllEntitiesFromDb as getAllEntitiesFromDb3 } from "@velvetmonkey/vault-core";
23281
22876
  function registerSemanticTools(server2, getVaultPath, getStateDb3) {
23282
22877
  server2.registerTool(
@@ -23285,7 +22880,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb3) {
23285
22880
  title: "Initialize Semantic Search",
23286
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.",
23287
22882
  inputSchema: {
23288
- force: z33.boolean().optional().describe(
22883
+ force: z32.boolean().optional().describe(
23289
22884
  "Rebuild all embeddings even if they already exist (default: false)"
23290
22885
  )
23291
22886
  }
@@ -23380,7 +22975,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb3) {
23380
22975
 
23381
22976
  // src/tools/read/merges.ts
23382
22977
  init_levenshtein();
23383
- import { z as z34 } from "zod";
22978
+ import { z as z33 } from "zod";
23384
22979
  import { getAllEntitiesFromDb as getAllEntitiesFromDb4, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
23385
22980
  function normalizeName(name) {
23386
22981
  return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
@@ -23390,7 +22985,7 @@ function registerMergeTools2(server2, getStateDb3) {
23390
22985
  "suggest_entity_merges",
23391
22986
  "Find potential duplicate entities that could be merged based on name similarity",
23392
22987
  {
23393
- 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")
23394
22989
  },
23395
22990
  async ({ limit }) => {
23396
22991
  const stateDb2 = getStateDb3();
@@ -23492,11 +23087,11 @@ function registerMergeTools2(server2, getStateDb3) {
23492
23087
  "dismiss_merge_suggestion",
23493
23088
  "Permanently dismiss a merge suggestion so it never reappears",
23494
23089
  {
23495
- source_path: z34.string().describe("Path of the source entity"),
23496
- target_path: z34.string().describe("Path of the target entity"),
23497
- source_name: z34.string().describe("Name of the source entity"),
23498
- target_name: z34.string().describe("Name of the target entity"),
23499
- 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")
23500
23095
  },
23501
23096
  async ({ source_path, target_path, source_name, target_name, reason }) => {
23502
23097
  const stateDb2 = getStateDb3();
@@ -23515,7 +23110,7 @@ function registerMergeTools2(server2, getStateDb3) {
23515
23110
  }
23516
23111
 
23517
23112
  // src/tools/read/temporalAnalysis.ts
23518
- import { z as z35 } from "zod";
23113
+ import { z as z34 } from "zod";
23519
23114
  init_wikilinks();
23520
23115
  function formatDate3(d) {
23521
23116
  return d.toISOString().split("T")[0];
@@ -24041,9 +23636,9 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24041
23636
  title: "Context Around Date",
24042
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.",
24043
23638
  inputSchema: {
24044
- date: z35.string().describe("Center date in YYYY-MM-DD format"),
24045
- window_days: z35.coerce.number().default(3).describe("Days before and after the center date (default 3 = 7-day window)"),
24046
- 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")
24047
23642
  }
24048
23643
  },
24049
23644
  async ({ date, window_days, limit: requestedLimit }) => {
@@ -24058,12 +23653,12 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24058
23653
  title: "Predict Stale Notes",
24059
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.",
24060
23655
  inputSchema: {
24061
- days: z35.coerce.number().default(30).describe("Notes not modified in this many days (default 30)"),
24062
- min_importance: z35.coerce.number().default(0).describe("Filter by minimum importance score 0-100 (default 0)"),
24063
- include_recommendations: z35.boolean().default(true).describe("Include action recommendations (default true)"),
24064
- folder: z35.string().optional().describe("Limit to notes in this folder"),
24065
- limit: z35.coerce.number().default(30).describe("Maximum results to return (default 30)"),
24066
- 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)")
24067
23662
  }
24068
23663
  },
24069
23664
  async ({ days, min_importance, include_recommendations, folder, limit: requestedLimit, offset }) => {
@@ -24087,9 +23682,9 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24087
23682
  title: "Track Concept Evolution",
24088
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.",
24089
23684
  inputSchema: {
24090
- entity: z35.string().describe("Entity name (case-insensitive)"),
24091
- days_back: z35.coerce.number().default(90).describe("How far back to look (default 90 days)"),
24092
- 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)")
24093
23688
  }
24094
23689
  },
24095
23690
  async ({ entity, days_back, include_cooccurrence }) => {
@@ -24109,10 +23704,10 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24109
23704
  title: "Temporal Summary",
24110
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.",
24111
23706
  inputSchema: {
24112
- start_date: z35.string().describe("Start of period in YYYY-MM-DD format"),
24113
- end_date: z35.string().describe("End of period in YYYY-MM-DD format"),
24114
- focus_entities: z35.array(z35.string()).optional().describe("Specific entities to track evolution for (default: top 5 active entities in period)"),
24115
- 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")
24116
23711
  }
24117
23712
  },
24118
23713
  async ({ start_date, end_date, focus_entities, limit: requestedLimit }) => {
@@ -24131,15 +23726,15 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
24131
23726
  }
24132
23727
 
24133
23728
  // src/tools/read/sessionHistory.ts
24134
- import { z as z36 } from "zod";
23729
+ import { z as z35 } from "zod";
24135
23730
  function registerSessionHistoryTools(server2, getStateDb3) {
24136
23731
  server2.tool(
24137
23732
  "vault_session_history",
24138
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).",
24139
23734
  {
24140
- session_id: z36.string().optional().describe("Session ID for detail view. Omit for recent sessions list."),
24141
- include_children: z36.boolean().optional().describe("Include child sessions (default: true)"),
24142
- 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)")
24143
23738
  },
24144
23739
  async (args) => {
24145
23740
  const stateDb2 = getStateDb3();
@@ -24187,7 +23782,7 @@ function registerSessionHistoryTools(server2, getStateDb3) {
24187
23782
  }
24188
23783
 
24189
23784
  // src/tools/read/entityHistory.ts
24190
- import { z as z37 } from "zod";
23785
+ import { z as z36 } from "zod";
24191
23786
 
24192
23787
  // src/core/read/entityHistory.ts
24193
23788
  function normalizeTimestamp(ts) {
@@ -24345,8 +23940,8 @@ function registerEntityHistoryTools(server2, getStateDb3) {
24345
23940
  "vault_entity_history",
24346
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.",
24347
23942
  {
24348
- entity_name: z37.string().describe("Entity name to query (case-insensitive)"),
24349
- 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([
24350
23945
  "application",
24351
23946
  "feedback",
24352
23947
  "suggestion",
@@ -24355,10 +23950,10 @@ function registerEntityHistoryTools(server2, getStateDb3) {
24355
23950
  "memory",
24356
23951
  "correction"
24357
23952
  ])).optional().describe("Filter to specific event types. Omit for all types."),
24358
- start_date: z37.string().optional().describe("Start date (YYYY-MM-DD) for date range filter"),
24359
- end_date: z37.string().optional().describe("End date (YYYY-MM-DD) for date range filter"),
24360
- limit: z37.number().min(1).max(200).optional().describe("Max events to return (default: 50)"),
24361
- 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)")
24362
23957
  },
24363
23958
  async (args) => {
24364
23959
  const stateDb2 = getStateDb3();
@@ -24390,7 +23985,7 @@ function registerEntityHistoryTools(server2, getStateDb3) {
24390
23985
  }
24391
23986
 
24392
23987
  // src/tools/read/learningReport.ts
24393
- import { z as z38 } from "zod";
23988
+ import { z as z37 } from "zod";
24394
23989
 
24395
23990
  // src/core/read/learningReport.ts
24396
23991
  function isoDate(d) {
@@ -24526,8 +24121,8 @@ function registerLearningReportTools(server2, getIndex, getStateDb3) {
24526
24121
  "flywheel_learning_report",
24527
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.",
24528
24123
  {
24529
- 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."),
24530
- 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)")
24531
24126
  },
24532
24127
  async (args) => {
24533
24128
  const stateDb2 = getStateDb3();
@@ -24554,7 +24149,7 @@ function registerLearningReportTools(server2, getIndex, getStateDb3) {
24554
24149
  }
24555
24150
 
24556
24151
  // src/tools/read/calibrationExport.ts
24557
- import { z as z39 } from "zod";
24152
+ import { z as z38 } from "zod";
24558
24153
 
24559
24154
  // src/core/read/calibrationExport.ts
24560
24155
  init_wikilinkFeedback();
@@ -24847,8 +24442,8 @@ function registerCalibrationExportTools(server2, getIndex, getStateDb3, getConfi
24847
24442
  "flywheel_calibration_export",
24848
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.",
24849
24444
  {
24850
- days_back: z39.number().min(1).max(365).optional().describe("Analysis window in days (default: 30)"),
24851
- 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)")
24852
24447
  },
24853
24448
  async (args) => {
24854
24449
  const stateDb2 = getStateDb3();
@@ -25135,7 +24730,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
25135
24730
  const schemaIdx = handlerIdx - 1;
25136
24731
  const schema = args[schemaIdx];
25137
24732
  if (schema && typeof schema === "object" && !Array.isArray(schema)) {
25138
- schema.vault = z40.string().optional().describe(
24733
+ schema.vault = z39.string().optional().describe(
25139
24734
  `Vault name for multi-vault mode. Available: ${registry.getVaultNames().join(", ")}. Default: ${registry.primaryName}`
25140
24735
  );
25141
24736
  }
@@ -25261,7 +24856,6 @@ function registerAllTools(targetServer, ctx) {
25261
24856
  registerLearningReportTools(targetServer, gvi, gsd);
25262
24857
  registerCalibrationExportTools(targetServer, gvi, gsd, gcf);
25263
24858
  registerMemoryTools(targetServer, gsd);
25264
- registerRecallTools(targetServer, gsd, gvp, () => gvi() ?? null);
25265
24859
  registerBriefTools(targetServer, gsd);
25266
24860
  registerVaultResources(targetServer, () => gvi() ?? null);
25267
24861
  }
@@ -25445,7 +25039,10 @@ function updateFlywheelConfig(config) {
25445
25039
  flywheelConfig = config;
25446
25040
  setWikilinkConfig(config);
25447
25041
  const ctx = getActiveVaultContext();
25448
- if (ctx) ctx.flywheelConfig = config;
25042
+ if (ctx) {
25043
+ ctx.flywheelConfig = config;
25044
+ setActiveScope(buildVaultScope(ctx));
25045
+ }
25449
25046
  }
25450
25047
  async function bootVault(ctx, startTime) {
25451
25048
  const vp = ctx.vaultPath;
@@ -25598,6 +25195,7 @@ async function main() {
25598
25195
  loadVaultCooccurrence(primaryCtx);
25599
25196
  activateVault(primaryCtx);
25600
25197
  await bootVault(primaryCtx, startTime);
25198
+ activateVault(primaryCtx);
25601
25199
  if (vaultConfigs && vaultConfigs.length > 1) {
25602
25200
  const secondaryConfigs = vaultConfigs.slice(1);
25603
25201
  (async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.149",
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.149",
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
+ }