@velvetmonkey/flywheel-memory 2.0.47 → 2.0.48

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 +216 -33
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3594,6 +3594,19 @@ function updateSuppressionList(stateDb2) {
3594
3594
  transaction();
3595
3595
  return updated;
3596
3596
  }
3597
+ function suppressEntity(stateDb2, entity) {
3598
+ stateDb2.db.prepare(`
3599
+ INSERT INTO wikilink_suppressions (entity, false_positive_rate, updated_at)
3600
+ VALUES (?, 1.0, datetime('now'))
3601
+ ON CONFLICT(entity) DO UPDATE SET false_positive_rate = 1.0, updated_at = datetime('now')
3602
+ `).run(entity);
3603
+ }
3604
+ function unsuppressEntity(stateDb2, entity) {
3605
+ const result = stateDb2.db.prepare(
3606
+ "DELETE FROM wikilink_suppressions WHERE entity = ? COLLATE NOCASE"
3607
+ ).run(entity);
3608
+ return result.changes > 0;
3609
+ }
3597
3610
  function isSuppressed(stateDb2, entity, folder) {
3598
3611
  const row = stateDb2.db.prepare(
3599
3612
  "SELECT entity FROM wikilink_suppressions WHERE entity = ? COLLATE NOCASE"
@@ -13062,7 +13075,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
13062
13075
  throw new Error(validationResult.blockReason || "Output validation failed");
13063
13076
  }
13064
13077
  let workingReplacement = validationResult.content;
13065
- let { content: processedReplacement } = maybeApplyWikilinks(workingReplacement, skipWikilinks, notePath);
13078
+ let { content: processedReplacement } = maybeApplyWikilinks(workingReplacement, skipWikilinks, notePath, ctx.content);
13066
13079
  if (suggestOutgoingLinks && !skipWikilinks && processedReplacement.length >= 100) {
13067
13080
  const result = await suggestRelatedLinks(processedReplacement, { maxSuggestions, notePath });
13068
13081
  if (result.suffix) {
@@ -15857,7 +15870,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
15857
15870
  title: "Wikilink Feedback",
15858
15871
  description: 'Report and query wikilink accuracy feedback. Modes: "report" (record feedback), "list" (view recent feedback), "stats" (entity accuracy statistics), "dashboard" (full feedback loop data), "entity_timeline" (score history for an entity), "layer_timeseries" (per-layer contribution over time), "snapshot_diff" (compare two graph snapshots).',
15859
15872
  inputSchema: {
15860
- mode: z21.enum(["report", "list", "stats", "dashboard", "entity_timeline", "layer_timeseries", "snapshot_diff"]).describe("Operation mode"),
15873
+ mode: z21.enum(["report", "list", "stats", "dashboard", "entity_timeline", "layer_timeseries", "snapshot_diff", "suppress", "unsuppress"]).describe("Operation mode"),
15861
15874
  entity: z21.string().optional().describe("Entity name (required for report and entity_timeline modes, optional filter for list/stats)"),
15862
15875
  note_path: z21.string().optional().describe("Note path where the wikilink appeared (for report mode)"),
15863
15876
  context: z21.string().optional().describe("Surrounding text context (for report mode)"),
@@ -15904,6 +15917,9 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
15904
15917
  ).run(entity, note_path);
15905
15918
  }
15906
15919
  const suppressionUpdated = updateSuppressionList(stateDb2) > 0;
15920
+ if (!correct) {
15921
+ suppressEntity(stateDb2, entity);
15922
+ }
15907
15923
  result = {
15908
15924
  mode: "report",
15909
15925
  reported: {
@@ -15982,6 +15998,38 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
15982
15998
  };
15983
15999
  break;
15984
16000
  }
16001
+ case "suppress": {
16002
+ if (!entity) {
16003
+ return {
16004
+ content: [{ type: "text", text: JSON.stringify({ error: "entity is required for suppress mode" }) }],
16005
+ isError: true
16006
+ };
16007
+ }
16008
+ suppressEntity(stateDb2, entity);
16009
+ result = {
16010
+ mode: "suppress",
16011
+ entity,
16012
+ suppressed: true,
16013
+ total_suppressed: getSuppressedCount(stateDb2)
16014
+ };
16015
+ break;
16016
+ }
16017
+ case "unsuppress": {
16018
+ if (!entity) {
16019
+ return {
16020
+ content: [{ type: "text", text: JSON.stringify({ error: "entity is required for unsuppress mode" }) }],
16021
+ isError: true
16022
+ };
16023
+ }
16024
+ const wasRemoved = unsuppressEntity(stateDb2, entity);
16025
+ result = {
16026
+ mode: "unsuppress",
16027
+ entity,
16028
+ was_suppressed: wasRemoved,
16029
+ total_suppressed: getSuppressedCount(stateDb2)
16030
+ };
16031
+ break;
16032
+ }
15985
16033
  }
15986
16034
  return {
15987
16035
  content: [
@@ -15995,8 +16043,142 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
15995
16043
  );
15996
16044
  }
15997
16045
 
15998
- // src/tools/write/config.ts
16046
+ // src/tools/write/corrections.ts
15999
16047
  import { z as z22 } from "zod";
16048
+
16049
+ // src/core/write/corrections.ts
16050
+ function recordCorrection(stateDb2, type, description, source = "user", entity, notePath) {
16051
+ const result = stateDb2.db.prepare(`
16052
+ INSERT INTO corrections (entity, note_path, correction_type, description, source)
16053
+ VALUES (?, ?, ?, ?, ?)
16054
+ `).run(entity ?? null, notePath ?? null, type, description, source);
16055
+ return stateDb2.db.prepare(
16056
+ "SELECT * FROM corrections WHERE id = ?"
16057
+ ).get(result.lastInsertRowid);
16058
+ }
16059
+ function listCorrections(stateDb2, status, entity, limit = 50) {
16060
+ const conditions = [];
16061
+ const params = [];
16062
+ if (status) {
16063
+ conditions.push("status = ?");
16064
+ params.push(status);
16065
+ }
16066
+ if (entity) {
16067
+ conditions.push("entity = ? COLLATE NOCASE");
16068
+ params.push(entity);
16069
+ }
16070
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
16071
+ params.push(limit);
16072
+ return stateDb2.db.prepare(
16073
+ `SELECT * FROM corrections ${where} ORDER BY created_at DESC LIMIT ?`
16074
+ ).all(...params);
16075
+ }
16076
+ function resolveCorrection(stateDb2, id, newStatus) {
16077
+ const result = stateDb2.db.prepare(`
16078
+ UPDATE corrections
16079
+ SET status = ?, resolved_at = datetime('now')
16080
+ WHERE id = ?
16081
+ `).run(newStatus, id);
16082
+ return result.changes > 0;
16083
+ }
16084
+
16085
+ // src/tools/write/corrections.ts
16086
+ function registerCorrectionTools(server2, getStateDb) {
16087
+ server2.tool(
16088
+ "vault_record_correction",
16089
+ 'Record a persistent correction (e.g., "that link is wrong", "undo that"). Survives across sessions.',
16090
+ {
16091
+ correction_type: z22.enum(["wrong_link", "wrong_entity", "wrong_category", "general"]).describe("Type of correction"),
16092
+ description: z22.string().describe("What went wrong and what should be done"),
16093
+ entity: z22.string().optional().describe("Entity name (if correction is about a specific entity)"),
16094
+ note_path: z22.string().optional().describe("Note path (if correction is about a specific note)")
16095
+ },
16096
+ async ({ correction_type, description, entity, note_path }) => {
16097
+ const stateDb2 = getStateDb();
16098
+ if (!stateDb2) {
16099
+ return {
16100
+ content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
16101
+ isError: true
16102
+ };
16103
+ }
16104
+ const correction = recordCorrection(stateDb2, correction_type, description, "user", entity, note_path);
16105
+ return {
16106
+ content: [{
16107
+ type: "text",
16108
+ text: JSON.stringify({
16109
+ recorded: true,
16110
+ correction
16111
+ }, null, 2)
16112
+ }]
16113
+ };
16114
+ }
16115
+ );
16116
+ server2.tool(
16117
+ "vault_list_corrections",
16118
+ "List recorded corrections, optionally filtered by status or entity.",
16119
+ {
16120
+ status: z22.enum(["pending", "applied", "dismissed"]).optional().describe("Filter by status"),
16121
+ entity: z22.string().optional().describe("Filter by entity name"),
16122
+ limit: z22.number().min(1).max(200).default(50).describe("Max entries to return")
16123
+ },
16124
+ async ({ status, entity, limit }) => {
16125
+ const stateDb2 = getStateDb();
16126
+ if (!stateDb2) {
16127
+ return {
16128
+ content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
16129
+ isError: true
16130
+ };
16131
+ }
16132
+ const corrections = listCorrections(stateDb2, status, entity, limit);
16133
+ return {
16134
+ content: [{
16135
+ type: "text",
16136
+ text: JSON.stringify({
16137
+ corrections,
16138
+ count: corrections.length
16139
+ }, null, 2)
16140
+ }]
16141
+ };
16142
+ }
16143
+ );
16144
+ server2.tool(
16145
+ "vault_resolve_correction",
16146
+ "Resolve a correction by marking it as applied or dismissed.",
16147
+ {
16148
+ correction_id: z22.number().describe("ID of the correction to resolve"),
16149
+ status: z22.enum(["applied", "dismissed"]).describe("New status")
16150
+ },
16151
+ async ({ correction_id, status }) => {
16152
+ const stateDb2 = getStateDb();
16153
+ if (!stateDb2) {
16154
+ return {
16155
+ content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
16156
+ isError: true
16157
+ };
16158
+ }
16159
+ const resolved = resolveCorrection(stateDb2, correction_id, status);
16160
+ if (!resolved) {
16161
+ return {
16162
+ content: [{ type: "text", text: JSON.stringify({ error: `Correction ${correction_id} not found` }) }],
16163
+ isError: true
16164
+ };
16165
+ }
16166
+ return {
16167
+ content: [{
16168
+ type: "text",
16169
+ text: JSON.stringify({
16170
+ resolved: true,
16171
+ correction_id,
16172
+ status
16173
+ }, null, 2)
16174
+ }]
16175
+ };
16176
+ }
16177
+ );
16178
+ }
16179
+
16180
+ // src/tools/write/config.ts
16181
+ import { z as z23 } from "zod";
16000
16182
  import { saveFlywheelConfigToDb as saveFlywheelConfigToDb2 } from "@velvetmonkey/vault-core";
16001
16183
  function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
16002
16184
  server2.registerTool(
@@ -16005,9 +16187,9 @@ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
16005
16187
  title: "Flywheel Config",
16006
16188
  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"] })',
16007
16189
  inputSchema: {
16008
- mode: z22.enum(["get", "set"]).describe("Operation mode"),
16009
- key: z22.string().optional().describe("Config key to update (required for set mode)"),
16010
- value: z22.unknown().optional().describe("New value for the key (required for set mode)")
16190
+ mode: z23.enum(["get", "set"]).describe("Operation mode"),
16191
+ key: z23.string().optional().describe("Config key to update (required for set mode)"),
16192
+ value: z23.unknown().optional().describe("New value for the key (required for set mode)")
16011
16193
  }
16012
16194
  },
16013
16195
  async ({ mode, key, value }) => {
@@ -16045,7 +16227,7 @@ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
16045
16227
  }
16046
16228
 
16047
16229
  // src/tools/write/enrich.ts
16048
- import { z as z23 } from "zod";
16230
+ import { z as z24 } from "zod";
16049
16231
  import * as fs29 from "fs/promises";
16050
16232
  import * as path30 from "path";
16051
16233
  function hasSkipWikilinks(content) {
@@ -16100,9 +16282,9 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
16100
16282
  "vault_init",
16101
16283
  "Initialize vault for Flywheel \u2014 scans legacy notes with zero wikilinks and applies entity links. Safe to re-run (idempotent). Use dry_run (default) to preview.",
16102
16284
  {
16103
- dry_run: z23.boolean().default(true).describe("If true (default), preview what would be linked without modifying files"),
16104
- batch_size: z23.number().default(50).describe("Maximum notes to process per invocation (default: 50)"),
16105
- offset: z23.number().default(0).describe("Skip this many eligible notes (for pagination across invocations)")
16285
+ dry_run: z24.boolean().default(true).describe("If true (default), preview what would be linked without modifying files"),
16286
+ batch_size: z24.number().default(50).describe("Maximum notes to process per invocation (default: 50)"),
16287
+ offset: z24.number().default(0).describe("Skip this many eligible notes (for pagination across invocations)")
16106
16288
  },
16107
16289
  async ({ dry_run, batch_size, offset }) => {
16108
16290
  const startTime = Date.now();
@@ -16197,7 +16379,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
16197
16379
  }
16198
16380
 
16199
16381
  // src/tools/read/metrics.ts
16200
- import { z as z24 } from "zod";
16382
+ import { z as z25 } from "zod";
16201
16383
 
16202
16384
  // src/core/shared/metrics.ts
16203
16385
  var ALL_METRICS = [
@@ -16363,10 +16545,10 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
16363
16545
  title: "Vault Growth",
16364
16546
  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.',
16365
16547
  inputSchema: {
16366
- mode: z24.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
16367
- metric: z24.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
16368
- days_back: z24.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
16369
- limit: z24.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
16548
+ mode: z25.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
16549
+ metric: z25.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
16550
+ days_back: z25.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
16551
+ limit: z25.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
16370
16552
  }
16371
16553
  },
16372
16554
  async ({ mode, metric, days_back, limit: eventLimit }) => {
@@ -16439,7 +16621,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
16439
16621
  }
16440
16622
 
16441
16623
  // src/tools/read/activity.ts
16442
- import { z as z25 } from "zod";
16624
+ import { z as z26 } from "zod";
16443
16625
 
16444
16626
  // src/core/shared/toolTracking.ts
16445
16627
  function recordToolInvocation(stateDb2, event) {
@@ -16599,10 +16781,10 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
16599
16781
  title: "Vault Activity",
16600
16782
  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)',
16601
16783
  inputSchema: {
16602
- mode: z25.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
16603
- session_id: z25.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
16604
- days_back: z25.number().optional().describe("Number of days to look back (default: 30)"),
16605
- limit: z25.number().optional().describe("Maximum results to return (default: 20)")
16784
+ mode: z26.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
16785
+ session_id: z26.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
16786
+ days_back: z26.number().optional().describe("Number of days to look back (default: 30)"),
16787
+ limit: z26.number().optional().describe("Maximum results to return (default: 20)")
16606
16788
  }
16607
16789
  },
16608
16790
  async ({ mode, session_id, days_back, limit: resultLimit }) => {
@@ -16669,7 +16851,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
16669
16851
  }
16670
16852
 
16671
16853
  // src/tools/read/similarity.ts
16672
- import { z as z26 } from "zod";
16854
+ import { z as z27 } from "zod";
16673
16855
 
16674
16856
  // src/core/read/similarity.ts
16675
16857
  import * as fs30 from "fs";
@@ -16933,9 +17115,9 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
16933
17115
  title: "Find Similar Notes",
16934
17116
  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). Use exclude_linked to filter out notes already connected via wikilinks.",
16935
17117
  inputSchema: {
16936
- path: z26.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
16937
- limit: z26.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
16938
- exclude_linked: z26.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
17118
+ path: z27.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
17119
+ limit: z27.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
17120
+ exclude_linked: z27.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
16939
17121
  }
16940
17122
  },
16941
17123
  async ({ path: path33, limit, exclude_linked }) => {
@@ -16979,7 +17161,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
16979
17161
  }
16980
17162
 
16981
17163
  // src/tools/read/semantic.ts
16982
- import { z as z27 } from "zod";
17164
+ import { z as z28 } from "zod";
16983
17165
  import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
16984
17166
  function registerSemanticTools(server2, getVaultPath, getStateDb) {
16985
17167
  server2.registerTool(
@@ -16988,7 +17170,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
16988
17170
  title: "Initialize Semantic Search",
16989
17171
  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.",
16990
17172
  inputSchema: {
16991
- force: z27.boolean().optional().describe(
17173
+ force: z28.boolean().optional().describe(
16992
17174
  "Rebuild all embeddings even if they already exist (default: false)"
16993
17175
  )
16994
17176
  }
@@ -17068,7 +17250,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
17068
17250
 
17069
17251
  // src/tools/read/merges.ts
17070
17252
  init_levenshtein();
17071
- import { z as z28 } from "zod";
17253
+ import { z as z29 } from "zod";
17072
17254
  import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
17073
17255
  function normalizeName(name) {
17074
17256
  return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
@@ -17078,7 +17260,7 @@ function registerMergeTools2(server2, getStateDb) {
17078
17260
  "suggest_entity_merges",
17079
17261
  "Find potential duplicate entities that could be merged based on name similarity",
17080
17262
  {
17081
- limit: z28.number().optional().default(50).describe("Maximum number of suggestions to return")
17263
+ limit: z29.number().optional().default(50).describe("Maximum number of suggestions to return")
17082
17264
  },
17083
17265
  async ({ limit }) => {
17084
17266
  const stateDb2 = getStateDb();
@@ -17180,11 +17362,11 @@ function registerMergeTools2(server2, getStateDb) {
17180
17362
  "dismiss_merge_suggestion",
17181
17363
  "Permanently dismiss a merge suggestion so it never reappears",
17182
17364
  {
17183
- source_path: z28.string().describe("Path of the source entity"),
17184
- target_path: z28.string().describe("Path of the target entity"),
17185
- source_name: z28.string().describe("Name of the source entity"),
17186
- target_name: z28.string().describe("Name of the target entity"),
17187
- reason: z28.string().describe("Original suggestion reason")
17365
+ source_path: z29.string().describe("Path of the source entity"),
17366
+ target_path: z29.string().describe("Path of the target entity"),
17367
+ source_name: z29.string().describe("Name of the source entity"),
17368
+ target_name: z29.string().describe("Name of the target entity"),
17369
+ reason: z29.string().describe("Original suggestion reason")
17188
17370
  },
17189
17371
  async ({ source_path, target_path, source_name, target_name, reason }) => {
17190
17372
  const stateDb2 = getStateDb();
@@ -17583,6 +17765,7 @@ registerSystemTools2(server, vaultPath);
17583
17765
  registerPolicyTools(server, vaultPath);
17584
17766
  registerTagTools(server, () => vaultIndex, () => vaultPath);
17585
17767
  registerWikilinkFeedbackTools(server, () => stateDb);
17768
+ registerCorrectionTools(server, () => stateDb);
17586
17769
  registerInitTools(server, vaultPath, () => stateDb);
17587
17770
  registerConfigTools(
17588
17771
  server,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.47",
3
+ "version": "2.0.48",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@modelcontextprotocol/sdk": "^1.25.1",
55
- "@velvetmonkey/vault-core": "^2.0.47",
55
+ "@velvetmonkey/vault-core": "^2.0.48",
56
56
  "better-sqlite3": "^11.0.0",
57
57
  "chokidar": "^4.0.0",
58
58
  "gray-matter": "^4.0.3",