@velvetmonkey/flywheel-memory 2.0.11 → 2.0.13

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 +256 -27
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2731,6 +2731,15 @@ import {
2731
2731
  // src/core/write/wikilinkFeedback.ts
2732
2732
  var MIN_FEEDBACK_COUNT = 10;
2733
2733
  var SUPPRESSION_THRESHOLD = 0.3;
2734
+ var FEEDBACK_BOOST_MIN_SAMPLES = 5;
2735
+ var FOLDER_SUPPRESSION_MIN_COUNT = 5;
2736
+ var FEEDBACK_BOOST_TIERS = [
2737
+ { minAccuracy: 0.95, minSamples: 20, boost: 5 },
2738
+ { minAccuracy: 0.8, minSamples: 5, boost: 2 },
2739
+ { minAccuracy: 0.6, minSamples: 5, boost: 0 },
2740
+ { minAccuracy: 0.4, minSamples: 5, boost: -2 },
2741
+ { minAccuracy: 0, minSamples: 5, boost: -4 }
2742
+ ];
2734
2743
  function recordFeedback(stateDb2, entity, context, notePath, correct) {
2735
2744
  stateDb2.db.prepare(
2736
2745
  "INSERT INTO wikilink_feedback (entity, context, note_path, correct) VALUES (?, ?, ?, ?)"
@@ -2814,11 +2823,31 @@ function updateSuppressionList(stateDb2) {
2814
2823
  transaction();
2815
2824
  return updated;
2816
2825
  }
2817
- function isSuppressed(stateDb2, entity) {
2826
+ function isSuppressed(stateDb2, entity, folder) {
2818
2827
  const row = stateDb2.db.prepare(
2819
2828
  "SELECT entity FROM wikilink_suppressions WHERE entity = ?"
2820
2829
  ).get(entity);
2821
- return !!row;
2830
+ if (row) return true;
2831
+ if (folder !== void 0) {
2832
+ const folderStats = stateDb2.db.prepare(`
2833
+ SELECT
2834
+ COUNT(*) as total,
2835
+ SUM(CASE WHEN correct = 0 THEN 1 ELSE 0 END) as false_positives
2836
+ FROM wikilink_feedback
2837
+ WHERE entity = ? AND (
2838
+ CASE WHEN ? = '' THEN note_path NOT LIKE '%/%'
2839
+ ELSE note_path LIKE ? || '/%'
2840
+ END
2841
+ )
2842
+ `).get(entity, folder, folder);
2843
+ if (folderStats && folderStats.total >= FOLDER_SUPPRESSION_MIN_COUNT) {
2844
+ const fpRate = folderStats.false_positives / folderStats.total;
2845
+ if (fpRate >= SUPPRESSION_THRESHOLD) {
2846
+ return true;
2847
+ }
2848
+ }
2849
+ }
2850
+ return false;
2822
2851
  }
2823
2852
  function getSuppressedCount(stateDb2) {
2824
2853
  const row = stateDb2.db.prepare(
@@ -2826,6 +2855,109 @@ function getSuppressedCount(stateDb2) {
2826
2855
  ).get();
2827
2856
  return row.count;
2828
2857
  }
2858
+ function computeBoostFromAccuracy(accuracy, sampleCount) {
2859
+ if (sampleCount < FEEDBACK_BOOST_MIN_SAMPLES) return 0;
2860
+ for (const tier of FEEDBACK_BOOST_TIERS) {
2861
+ if (accuracy >= tier.minAccuracy && sampleCount >= tier.minSamples) {
2862
+ return tier.boost;
2863
+ }
2864
+ }
2865
+ return 0;
2866
+ }
2867
+ function getAllFeedbackBoosts(stateDb2, folder) {
2868
+ const globalRows = stateDb2.db.prepare(`
2869
+ SELECT
2870
+ entity,
2871
+ COUNT(*) as total,
2872
+ SUM(CASE WHEN correct = 1 THEN 1 ELSE 0 END) as correct_count
2873
+ FROM wikilink_feedback
2874
+ GROUP BY entity
2875
+ HAVING total >= ?
2876
+ `).all(FEEDBACK_BOOST_MIN_SAMPLES);
2877
+ let folderStats = null;
2878
+ if (folder !== void 0) {
2879
+ const folderRows = stateDb2.db.prepare(`
2880
+ SELECT
2881
+ entity,
2882
+ COUNT(*) as total,
2883
+ SUM(CASE WHEN correct = 1 THEN 1 ELSE 0 END) as correct_count
2884
+ FROM wikilink_feedback
2885
+ WHERE (
2886
+ CASE WHEN ? = '' THEN note_path NOT LIKE '%/%'
2887
+ ELSE note_path LIKE ? || '/%'
2888
+ END
2889
+ )
2890
+ GROUP BY entity
2891
+ HAVING total >= ?
2892
+ `).all(folder, folder, FEEDBACK_BOOST_MIN_SAMPLES);
2893
+ folderStats = /* @__PURE__ */ new Map();
2894
+ for (const row of folderRows) {
2895
+ folderStats.set(row.entity, {
2896
+ accuracy: row.correct_count / row.total,
2897
+ count: row.total
2898
+ });
2899
+ }
2900
+ }
2901
+ const boosts = /* @__PURE__ */ new Map();
2902
+ for (const row of globalRows) {
2903
+ let accuracy;
2904
+ let sampleCount;
2905
+ const fs25 = folderStats?.get(row.entity);
2906
+ if (fs25 && fs25.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
2907
+ accuracy = fs25.accuracy;
2908
+ sampleCount = fs25.count;
2909
+ } else {
2910
+ accuracy = row.correct_count / row.total;
2911
+ sampleCount = row.total;
2912
+ }
2913
+ const boost = computeBoostFromAccuracy(accuracy, sampleCount);
2914
+ if (boost !== 0) {
2915
+ boosts.set(row.entity, boost);
2916
+ }
2917
+ }
2918
+ return boosts;
2919
+ }
2920
+ function trackWikilinkApplications(stateDb2, notePath, entities) {
2921
+ const upsert = stateDb2.db.prepare(`
2922
+ INSERT INTO wikilink_applications (entity, note_path, applied_at, status)
2923
+ VALUES (?, ?, datetime('now'), 'applied')
2924
+ ON CONFLICT(entity, note_path) DO UPDATE SET
2925
+ applied_at = datetime('now'),
2926
+ status = 'applied'
2927
+ `);
2928
+ const transaction = stateDb2.db.transaction(() => {
2929
+ for (const entity of entities) {
2930
+ upsert.run(entity.toLowerCase(), notePath);
2931
+ }
2932
+ });
2933
+ transaction();
2934
+ }
2935
+ function getTrackedApplications(stateDb2, notePath) {
2936
+ const rows = stateDb2.db.prepare(
2937
+ `SELECT entity FROM wikilink_applications WHERE note_path = ? AND status = 'applied'`
2938
+ ).all(notePath);
2939
+ return rows.map((r) => r.entity);
2940
+ }
2941
+ function processImplicitFeedback(stateDb2, notePath, currentContent) {
2942
+ const tracked = getTrackedApplications(stateDb2, notePath);
2943
+ if (tracked.length === 0) return [];
2944
+ const currentLinks = extractLinkedEntities(currentContent);
2945
+ const removed = [];
2946
+ const markRemoved = stateDb2.db.prepare(
2947
+ `UPDATE wikilink_applications SET status = 'removed' WHERE entity = ? AND note_path = ?`
2948
+ );
2949
+ const transaction = stateDb2.db.transaction(() => {
2950
+ for (const entity of tracked) {
2951
+ if (!currentLinks.has(entity)) {
2952
+ recordFeedback(stateDb2, entity, "implicit:removed", notePath, false);
2953
+ markRemoved.run(entity, notePath);
2954
+ removed.push(entity);
2955
+ }
2956
+ }
2957
+ });
2958
+ transaction();
2959
+ return removed;
2960
+ }
2829
2961
 
2830
2962
  // src/core/write/git.ts
2831
2963
  import { simpleGit, CheckRepoActions } from "simple-git";
@@ -4174,6 +4306,9 @@ function setWriteStateDb(stateDb2) {
4174
4306
  setHintsStateDb(stateDb2);
4175
4307
  setRecencyStateDb(stateDb2);
4176
4308
  }
4309
+ function getWriteStateDb() {
4310
+ return moduleStateDb4;
4311
+ }
4177
4312
  var entityIndex = null;
4178
4313
  var indexReady = false;
4179
4314
  var indexError2 = null;
@@ -4320,9 +4455,10 @@ function processWikilinks(content, notePath) {
4320
4455
  let entities = getAllEntities(entityIndex);
4321
4456
  console.error(`[Flywheel:DEBUG] Processing wikilinks with ${entities.length} entities`);
4322
4457
  if (moduleStateDb4) {
4458
+ const folder = notePath ? notePath.split("/")[0] : void 0;
4323
4459
  entities = entities.filter((e) => {
4324
4460
  const name = getEntityName2(e);
4325
- return !isSuppressed(moduleStateDb4, name);
4461
+ return !isSuppressed(moduleStateDb4, name, folder);
4326
4462
  });
4327
4463
  }
4328
4464
  const sortedEntities = sortEntitiesByPriority(entities, notePath);
@@ -4346,6 +4482,9 @@ function maybeApplyWikilinks(content, skipWikilinks, notePath) {
4346
4482
  checkAndRefreshIfStale();
4347
4483
  const result = processWikilinks(content, notePath);
4348
4484
  if (result.linksAdded > 0) {
4485
+ if (moduleStateDb4 && notePath) {
4486
+ trackWikilinkApplications(moduleStateDb4, notePath, result.linkedEntities);
4487
+ }
4349
4488
  return {
4350
4489
  content: result.content,
4351
4490
  wikilinkInfo: `Applied ${result.linksAdded} wikilink(s): ${result.linkedEntities.join(", ")}`
@@ -4663,7 +4802,8 @@ function suggestRelatedLinks(content, options = {}) {
4663
4802
  maxSuggestions = 3,
4664
4803
  excludeLinked = true,
4665
4804
  strictness = DEFAULT_STRICTNESS,
4666
- notePath
4805
+ notePath,
4806
+ detail = false
4667
4807
  } = options;
4668
4808
  const config = STRICTNESS_CONFIGS[strictness];
4669
4809
  const adaptiveMinScore = getAdaptiveMinScore(content.length, config.minSuggestionScore);
@@ -4697,6 +4837,8 @@ function suggestRelatedLinks(content, options = {}) {
4697
4837
  return emptyResult;
4698
4838
  }
4699
4839
  const linkedEntities = excludeLinked ? extractLinkedEntities(content) : /* @__PURE__ */ new Set();
4840
+ const noteFolder = notePath ? notePath.split("/")[0] : void 0;
4841
+ const feedbackBoosts = moduleStateDb4 ? getAllFeedbackBoosts(moduleStateDb4, noteFolder) : /* @__PURE__ */ new Map();
4700
4842
  const scoredEntities = [];
4701
4843
  const directlyMatchedEntities = /* @__PURE__ */ new Set();
4702
4844
  const entitiesWithContentMatch = /* @__PURE__ */ new Set();
@@ -4717,20 +4859,38 @@ function suggestRelatedLinks(content, options = {}) {
4717
4859
  if (contentScore > 0) {
4718
4860
  entitiesWithContentMatch.add(entityName);
4719
4861
  }
4720
- score += TYPE_BOOST[category] || 0;
4721
- score += contextBoosts[category] || 0;
4722
- if (recencyIndex) {
4723
- score += getRecencyBoost(entityName, recencyIndex);
4724
- }
4725
- if (notePath && entity.path) {
4726
- score += getCrossFolderBoost(entity.path, notePath);
4727
- }
4728
- score += getHubBoost(entity);
4862
+ const layerTypeBoost = TYPE_BOOST[category] || 0;
4863
+ score += layerTypeBoost;
4864
+ const layerContextBoost = contextBoosts[category] || 0;
4865
+ score += layerContextBoost;
4866
+ const layerRecencyBoost = recencyIndex ? getRecencyBoost(entityName, recencyIndex) : 0;
4867
+ score += layerRecencyBoost;
4868
+ const layerCrossFolderBoost = notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
4869
+ score += layerCrossFolderBoost;
4870
+ const layerHubBoost = getHubBoost(entity);
4871
+ score += layerHubBoost;
4872
+ const layerFeedbackAdj = feedbackBoosts.get(entityName) ?? 0;
4873
+ score += layerFeedbackAdj;
4729
4874
  if (score > 0) {
4730
4875
  directlyMatchedEntities.add(entityName);
4731
4876
  }
4732
4877
  if (score >= adaptiveMinScore) {
4733
- scoredEntities.push({ name: entityName, score, category });
4878
+ scoredEntities.push({
4879
+ name: entityName,
4880
+ path: entity.path || "",
4881
+ score,
4882
+ category,
4883
+ breakdown: {
4884
+ contentMatch: contentScore,
4885
+ cooccurrenceBoost: 0,
4886
+ typeBoost: layerTypeBoost,
4887
+ contextBoost: layerContextBoost,
4888
+ recencyBoost: layerRecencyBoost,
4889
+ crossFolderBoost: layerCrossFolderBoost,
4890
+ hubBoost: layerHubBoost,
4891
+ feedbackAdjustment: layerFeedbackAdj
4892
+ }
4893
+ });
4734
4894
  }
4735
4895
  }
4736
4896
  if (cooccurrenceIndex && directlyMatchedEntities.size > 0) {
@@ -4745,6 +4905,7 @@ function suggestRelatedLinks(content, options = {}) {
4745
4905
  const existing = scoredEntities.find((e) => e.name === entityName);
4746
4906
  if (existing) {
4747
4907
  existing.score += boost;
4908
+ existing.breakdown.cooccurrenceBoost += boost;
4748
4909
  } else {
4749
4910
  const entityTokens = tokenize(entityName);
4750
4911
  const hasContentOverlap = entityTokens.some(
@@ -4759,9 +4920,25 @@ function suggestRelatedLinks(content, options = {}) {
4759
4920
  const recencyBoostVal = recencyIndex ? getRecencyBoost(entityName, recencyIndex) : 0;
4760
4921
  const crossFolderBoost = notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
4761
4922
  const hubBoost = getHubBoost(entity);
4762
- const totalBoost = boost + typeBoost + contextBoost + recencyBoostVal + crossFolderBoost + hubBoost;
4923
+ const feedbackAdj = feedbackBoosts.get(entityName) ?? 0;
4924
+ const totalBoost = boost + typeBoost + contextBoost + recencyBoostVal + crossFolderBoost + hubBoost + feedbackAdj;
4763
4925
  if (totalBoost >= adaptiveMinScore) {
4764
- scoredEntities.push({ name: entityName, score: totalBoost, category });
4926
+ scoredEntities.push({
4927
+ name: entityName,
4928
+ path: entity.path || "",
4929
+ score: totalBoost,
4930
+ category,
4931
+ breakdown: {
4932
+ contentMatch: 0,
4933
+ cooccurrenceBoost: boost,
4934
+ typeBoost,
4935
+ contextBoost,
4936
+ recencyBoost: recencyBoostVal,
4937
+ crossFolderBoost,
4938
+ hubBoost,
4939
+ feedbackAdjustment: feedbackAdj
4940
+ }
4941
+ });
4765
4942
  }
4766
4943
  }
4767
4944
  }
@@ -4782,15 +4959,34 @@ function suggestRelatedLinks(content, options = {}) {
4782
4959
  }
4783
4960
  return 0;
4784
4961
  });
4785
- const topSuggestions = relevantEntities.slice(0, maxSuggestions).map((e) => e.name);
4962
+ const topEntries = relevantEntities.slice(0, maxSuggestions);
4963
+ const topSuggestions = topEntries.map((e) => e.name);
4786
4964
  if (topSuggestions.length === 0) {
4787
4965
  return emptyResult;
4788
4966
  }
4789
4967
  const suffix = "\u2192 " + topSuggestions.map((name) => `[[${name}]]`).join(", ");
4790
- return {
4968
+ const result = {
4791
4969
  suggestions: topSuggestions,
4792
4970
  suffix
4793
4971
  };
4972
+ if (detail) {
4973
+ const feedbackStats = moduleStateDb4 ? getEntityStats(moduleStateDb4) : [];
4974
+ const feedbackMap = new Map(feedbackStats.map((s) => [s.entity, s]));
4975
+ result.detailed = topEntries.map((e) => {
4976
+ const fb = feedbackMap.get(e.name);
4977
+ const confidence = e.score >= 20 ? "high" : e.score >= 12 ? "medium" : "low";
4978
+ return {
4979
+ entity: e.name,
4980
+ path: e.path,
4981
+ totalScore: e.score,
4982
+ breakdown: e.breakdown,
4983
+ confidence,
4984
+ feedbackCount: fb?.total ?? 0,
4985
+ accuracy: fb ? fb.accuracy : void 0
4986
+ };
4987
+ });
4988
+ }
4989
+ return result;
4794
4990
  }
4795
4991
  function detectAliasCollisions(noteName, aliases = []) {
4796
4992
  if (!moduleStateDb4) return [];
@@ -5616,11 +5812,12 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
5616
5812
  inputSchema: {
5617
5813
  text: z2.string().describe("The text to analyze for potential wikilinks"),
5618
5814
  limit: z2.coerce.number().default(50).describe("Maximum number of suggestions to return"),
5619
- offset: z2.coerce.number().default(0).describe("Number of suggestions to skip (for pagination)")
5815
+ offset: z2.coerce.number().default(0).describe("Number of suggestions to skip (for pagination)"),
5816
+ detail: z2.boolean().default(false).describe("Include per-layer score breakdown for each suggestion")
5620
5817
  },
5621
5818
  outputSchema: SuggestWikilinksOutputSchema
5622
5819
  },
5623
- async ({ text, limit: requestedLimit, offset }) => {
5820
+ async ({ text, limit: requestedLimit, offset, detail }) => {
5624
5821
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
5625
5822
  const index = getIndex();
5626
5823
  const allMatches = findEntityMatches(text, index.entities);
@@ -5631,6 +5828,16 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
5631
5828
  returned_count: matches.length,
5632
5829
  suggestions: matches
5633
5830
  };
5831
+ if (detail) {
5832
+ const scored = suggestRelatedLinks(text, {
5833
+ detail: true,
5834
+ maxSuggestions: limit,
5835
+ strictness: "balanced"
5836
+ });
5837
+ if (scored.detailed) {
5838
+ output.scored_suggestions = scored.detailed;
5839
+ }
5840
+ }
5634
5841
  return {
5635
5842
  content: [
5636
5843
  {
@@ -9103,6 +9310,10 @@ async function withVaultFile(options, operation) {
9103
9310
  return formatMcpResult(existsError);
9104
9311
  }
9105
9312
  const { content, frontmatter, lineEnding } = await readVaultFile(vaultPath2, notePath);
9313
+ const writeStateDb = getWriteStateDb();
9314
+ if (writeStateDb) {
9315
+ processImplicitFeedback(writeStateDb, notePath, content);
9316
+ }
9106
9317
  let sectionBoundary;
9107
9318
  if (section) {
9108
9319
  const sectionResult = ensureSectionExists(content, section, notePath);
@@ -12013,9 +12224,12 @@ var ALL_METRICS = [
12013
12224
  "entity_count",
12014
12225
  "avg_links_per_note",
12015
12226
  "link_density",
12016
- "connected_ratio"
12227
+ "connected_ratio",
12228
+ "wikilink_accuracy",
12229
+ "wikilink_feedback_volume",
12230
+ "wikilink_suppressed_count"
12017
12231
  ];
12018
- function computeMetrics(index) {
12232
+ function computeMetrics(index, stateDb2) {
12019
12233
  const noteCount = index.notes.size;
12020
12234
  let linkCount = 0;
12021
12235
  for (const note of index.notes.values()) {
@@ -12050,6 +12264,18 @@ function computeMetrics(index) {
12050
12264
  const possibleLinks = noteCount * (noteCount - 1);
12051
12265
  const linkDensity = possibleLinks > 0 ? linkCount / possibleLinks : 0;
12052
12266
  const connectedRatio = noteCount > 0 ? connectedNotes.size / noteCount : 0;
12267
+ let wikilinkAccuracy = 0;
12268
+ let wikilinkFeedbackVolume = 0;
12269
+ let wikilinkSuppressedCount = 0;
12270
+ if (stateDb2) {
12271
+ const entityStatsList = getEntityStats(stateDb2);
12272
+ wikilinkFeedbackVolume = entityStatsList.reduce((sum, s) => sum + s.total, 0);
12273
+ if (wikilinkFeedbackVolume > 0) {
12274
+ const totalCorrect = entityStatsList.reduce((sum, s) => sum + s.correct, 0);
12275
+ wikilinkAccuracy = Math.round(totalCorrect / wikilinkFeedbackVolume * 1e3) / 1e3;
12276
+ }
12277
+ wikilinkSuppressedCount = getSuppressedCount(stateDb2);
12278
+ }
12053
12279
  return {
12054
12280
  note_count: noteCount,
12055
12281
  link_count: linkCount,
@@ -12058,7 +12284,10 @@ function computeMetrics(index) {
12058
12284
  entity_count: entityCount,
12059
12285
  avg_links_per_note: Math.round(avgLinksPerNote * 100) / 100,
12060
12286
  link_density: Math.round(linkDensity * 1e4) / 1e4,
12061
- connected_ratio: Math.round(connectedRatio * 1e3) / 1e3
12287
+ connected_ratio: Math.round(connectedRatio * 1e3) / 1e3,
12288
+ wikilink_accuracy: wikilinkAccuracy,
12289
+ wikilink_feedback_volume: wikilinkFeedbackVolume,
12290
+ wikilink_suppressed_count: wikilinkSuppressedCount
12062
12291
  };
12063
12292
  }
12064
12293
  function recordMetrics(stateDb2, metrics) {
@@ -12148,7 +12377,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
12148
12377
  "vault_growth",
12149
12378
  {
12150
12379
  title: "Vault Growth",
12151
- description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago). Tracks 8 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio.',
12380
+ description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago). 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.',
12152
12381
  inputSchema: {
12153
12382
  mode: z21.enum(["current", "history", "trends"]).describe("Query mode: current snapshot, historical time series, or trend analysis"),
12154
12383
  metric: z21.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
@@ -12162,7 +12391,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
12162
12391
  let result;
12163
12392
  switch (mode) {
12164
12393
  case "current": {
12165
- const metrics = computeMetrics(index);
12394
+ const metrics = computeMetrics(index, stateDb2 ?? void 0);
12166
12395
  result = {
12167
12396
  mode: "current",
12168
12397
  metrics,
@@ -12189,7 +12418,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
12189
12418
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for trend analysis" }) }]
12190
12419
  };
12191
12420
  }
12192
- const currentMetrics = computeMetrics(index);
12421
+ const currentMetrics = computeMetrics(index, stateDb2);
12193
12422
  const trends = computeTrends(stateDb2, currentMetrics, daysBack);
12194
12423
  result = {
12195
12424
  mode: "trends",
@@ -12618,7 +12847,7 @@ async function runPostIndexWork(index) {
12618
12847
  await exportHubScores(index, stateDb);
12619
12848
  if (stateDb) {
12620
12849
  try {
12621
- const metrics = computeMetrics(index);
12850
+ const metrics = computeMetrics(index, stateDb);
12622
12851
  recordMetrics(stateDb, metrics);
12623
12852
  purgeOldMetrics(stateDb, 90);
12624
12853
  console.error("[Memory] Growth metrics recorded");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.11",
3
+ "version": "2.0.13",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. 36 tools for search, backlinks, graph queries, and mutations.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@modelcontextprotocol/sdk": "^1.25.1",
53
- "@velvetmonkey/vault-core": "^2.0.11",
53
+ "@velvetmonkey/vault-core": "^2.0.13",
54
54
  "better-sqlite3": "^11.0.0",
55
55
  "chokidar": "^4.0.0",
56
56
  "gray-matter": "^4.0.3",