@velvetmonkey/flywheel-mcp 1.25.5 → 1.26.2

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 +551 -64
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import chokidar from "chokidar";
6
+ import chokidar2 from "chokidar";
7
7
 
8
8
  // src/core/vault.ts
9
9
  import * as fs from "fs";
@@ -260,8 +260,8 @@ function updateIndexProgress(parsed, total) {
260
260
  function normalizeTarget(target) {
261
261
  return target.toLowerCase().replace(/\.md$/, "");
262
262
  }
263
- function normalizeNotePath(path11) {
264
- return path11.toLowerCase().replace(/\.md$/, "");
263
+ function normalizeNotePath(path12) {
264
+ return path12.toLowerCase().replace(/\.md$/, "");
265
265
  }
266
266
  async function buildVaultIndex(vaultPath2, options = {}) {
267
267
  const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
@@ -455,7 +455,7 @@ function findSimilarEntity(index, target) {
455
455
  }
456
456
  const maxDist = normalizedLen <= 10 ? 1 : 2;
457
457
  let bestMatch;
458
- for (const [entity, path11] of index.entities) {
458
+ for (const [entity, path12] of index.entities) {
459
459
  const lenDiff = Math.abs(entity.length - normalizedLen);
460
460
  if (lenDiff > maxDist) {
461
461
  continue;
@@ -463,7 +463,7 @@ function findSimilarEntity(index, target) {
463
463
  const dist = levenshteinDistance(normalized, entity);
464
464
  if (dist > 0 && dist <= maxDist) {
465
465
  if (!bestMatch || dist < bestMatch.distance) {
466
- bestMatch = { path: path11, entity, distance: dist };
466
+ bestMatch = { path: path12, entity, distance: dist };
467
467
  if (dist === 1) {
468
468
  return bestMatch;
469
469
  }
@@ -889,14 +889,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
889
889
  };
890
890
  function findSimilarEntity2(target, entities) {
891
891
  const targetLower = target.toLowerCase();
892
- for (const [name, path11] of entities) {
892
+ for (const [name, path12] of entities) {
893
893
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
894
- return path11;
894
+ return path12;
895
895
  }
896
896
  }
897
- for (const [name, path11] of entities) {
897
+ for (const [name, path12] of entities) {
898
898
  if (name.includes(targetLower) || targetLower.includes(name)) {
899
- return path11;
899
+ return path12;
900
900
  }
901
901
  }
902
902
  return void 0;
@@ -1193,8 +1193,8 @@ function registerHealthTools(server2, getIndex, getVaultPath) {
1193
1193
  top_tags: z3.array(TagStatSchema).describe("Top 20 most used tags"),
1194
1194
  folders: z3.array(FolderStatSchema).describe("Note counts by top-level folder")
1195
1195
  };
1196
- function isPeriodicNote(path11) {
1197
- const filename = path11.split("/").pop() || "";
1196
+ function isPeriodicNote(path12) {
1197
+ const filename = path12.split("/").pop() || "";
1198
1198
  const nameWithoutExt = filename.replace(/\.md$/, "");
1199
1199
  const patterns = [
1200
1200
  /^\d{4}-\d{2}-\d{2}$/,
@@ -1209,7 +1209,7 @@ function registerHealthTools(server2, getIndex, getVaultPath) {
1209
1209
  // YYYY (yearly)
1210
1210
  ];
1211
1211
  const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
1212
- const folder = path11.split("/")[0]?.toLowerCase() || "";
1212
+ const folder = path12.split("/")[0]?.toLowerCase() || "";
1213
1213
  return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
1214
1214
  }
1215
1215
  server2.registerTool(
@@ -2131,8 +2131,8 @@ function getStaleNotes(index, days, minBacklinks = 0) {
2131
2131
  return b.days_since_modified - a.days_since_modified;
2132
2132
  });
2133
2133
  }
2134
- function getContemporaneousNotes(index, path11, hours = 24) {
2135
- const targetNote = index.notes.get(path11);
2134
+ function getContemporaneousNotes(index, path12, hours = 24) {
2135
+ const targetNote = index.notes.get(path12);
2136
2136
  if (!targetNote) {
2137
2137
  return [];
2138
2138
  }
@@ -2140,7 +2140,7 @@ function getContemporaneousNotes(index, path11, hours = 24) {
2140
2140
  const windowMs = hours * 60 * 60 * 1e3;
2141
2141
  const results = [];
2142
2142
  for (const note of index.notes.values()) {
2143
- if (note.path === path11) continue;
2143
+ if (note.path === path12) continue;
2144
2144
  const timeDiff = Math.abs(note.modified.getTime() - targetTime);
2145
2145
  if (timeDiff <= windowMs) {
2146
2146
  results.push({
@@ -2932,14 +2932,14 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
2932
2932
  offset: z6.number().default(0).describe("Number of results to skip (for pagination)")
2933
2933
  }
2934
2934
  },
2935
- async ({ path: path11, hours, limit: requestedLimit, offset }) => {
2935
+ async ({ path: path12, hours, limit: requestedLimit, offset }) => {
2936
2936
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
2937
2937
  const index = getIndex();
2938
- const allResults = getContemporaneousNotes(index, path11, hours);
2938
+ const allResults = getContemporaneousNotes(index, path12, hours);
2939
2939
  const result = allResults.slice(offset, offset + limit);
2940
2940
  return {
2941
2941
  content: [{ type: "text", text: JSON.stringify({
2942
- reference_note: path11,
2942
+ reference_note: path12,
2943
2943
  window_hours: hours,
2944
2944
  total_count: allResults.length,
2945
2945
  returned_count: result.length,
@@ -2977,13 +2977,13 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
2977
2977
  path: z6.string().describe("Path to the note")
2978
2978
  }
2979
2979
  },
2980
- async ({ path: path11 }) => {
2980
+ async ({ path: path12 }) => {
2981
2981
  const index = getIndex();
2982
2982
  const vaultPath2 = getVaultPath();
2983
- const result = await getNoteStructure(index, path11, vaultPath2);
2983
+ const result = await getNoteStructure(index, path12, vaultPath2);
2984
2984
  if (!result) {
2985
2985
  return {
2986
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path11 }, null, 2) }]
2986
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path12 }, null, 2) }]
2987
2987
  };
2988
2988
  }
2989
2989
  return {
@@ -3000,18 +3000,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
3000
3000
  path: z6.string().describe("Path to the note")
3001
3001
  }
3002
3002
  },
3003
- async ({ path: path11 }) => {
3003
+ async ({ path: path12 }) => {
3004
3004
  const index = getIndex();
3005
3005
  const vaultPath2 = getVaultPath();
3006
- const result = await getHeadings(index, path11, vaultPath2);
3006
+ const result = await getHeadings(index, path12, vaultPath2);
3007
3007
  if (!result) {
3008
3008
  return {
3009
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path11 }, null, 2) }]
3009
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path12 }, null, 2) }]
3010
3010
  };
3011
3011
  }
3012
3012
  return {
3013
3013
  content: [{ type: "text", text: JSON.stringify({
3014
- path: path11,
3014
+ path: path12,
3015
3015
  heading_count: result.length,
3016
3016
  headings: result
3017
3017
  }, null, 2) }]
@@ -3029,15 +3029,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
3029
3029
  include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
3030
3030
  }
3031
3031
  },
3032
- async ({ path: path11, heading, include_subheadings }) => {
3032
+ async ({ path: path12, heading, include_subheadings }) => {
3033
3033
  const index = getIndex();
3034
3034
  const vaultPath2 = getVaultPath();
3035
- const result = await getSectionContent(index, path11, heading, vaultPath2, include_subheadings);
3035
+ const result = await getSectionContent(index, path12, heading, vaultPath2, include_subheadings);
3036
3036
  if (!result) {
3037
3037
  return {
3038
3038
  content: [{ type: "text", text: JSON.stringify({
3039
3039
  error: "Section not found",
3040
- path: path11,
3040
+ path: path12,
3041
3041
  heading
3042
3042
  }, null, 2) }]
3043
3043
  };
@@ -3114,19 +3114,19 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
3114
3114
  path: z6.string().describe("Path to the note")
3115
3115
  }
3116
3116
  },
3117
- async ({ path: path11 }) => {
3117
+ async ({ path: path12 }) => {
3118
3118
  const index = getIndex();
3119
3119
  const vaultPath2 = getVaultPath();
3120
3120
  const config = getConfig();
3121
- const result = await getTasksFromNote(index, path11, vaultPath2, config.exclude_task_tags || []);
3121
+ const result = await getTasksFromNote(index, path12, vaultPath2, config.exclude_task_tags || []);
3122
3122
  if (!result) {
3123
3123
  return {
3124
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path11 }, null, 2) }]
3124
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path12 }, null, 2) }]
3125
3125
  };
3126
3126
  }
3127
3127
  return {
3128
3128
  content: [{ type: "text", text: JSON.stringify({
3129
- path: path11,
3129
+ path: path12,
3130
3130
  task_count: result.length,
3131
3131
  open: result.filter((t) => t.status === "open").length,
3132
3132
  completed: result.filter((t) => t.status === "completed").length,
@@ -3258,14 +3258,14 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
3258
3258
  offset: z6.number().default(0).describe("Number of results to skip (for pagination)")
3259
3259
  }
3260
3260
  },
3261
- async ({ path: path11, limit: requestedLimit, offset }) => {
3261
+ async ({ path: path12, limit: requestedLimit, offset }) => {
3262
3262
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
3263
3263
  const index = getIndex();
3264
- const allResults = findBidirectionalLinks(index, path11);
3264
+ const allResults = findBidirectionalLinks(index, path12);
3265
3265
  const result = allResults.slice(offset, offset + limit);
3266
3266
  return {
3267
3267
  content: [{ type: "text", text: JSON.stringify({
3268
- scope: path11 || "all",
3268
+ scope: path12 || "all",
3269
3269
  total_count: allResults.length,
3270
3270
  returned_count: result.length,
3271
3271
  pairs: result
@@ -4806,6 +4806,462 @@ function findVaultRoot(startPath) {
4806
4806
  }
4807
4807
  }
4808
4808
 
4809
+ // src/core/watch/index.ts
4810
+ import chokidar from "chokidar";
4811
+
4812
+ // src/core/watch/pathFilter.ts
4813
+ import path11 from "path";
4814
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
4815
+ ".git",
4816
+ ".obsidian",
4817
+ ".trash",
4818
+ ".Trash",
4819
+ "node_modules",
4820
+ ".vscode",
4821
+ ".idea",
4822
+ "__pycache__",
4823
+ ".cache",
4824
+ ".claude"
4825
+ ]);
4826
+ var IGNORED_EXTENSIONS = /* @__PURE__ */ new Set([
4827
+ // Images
4828
+ ".png",
4829
+ ".jpg",
4830
+ ".jpeg",
4831
+ ".gif",
4832
+ ".webp",
4833
+ ".svg",
4834
+ ".ico",
4835
+ ".bmp",
4836
+ ".tiff",
4837
+ // Documents
4838
+ ".pdf",
4839
+ ".doc",
4840
+ ".docx",
4841
+ ".xls",
4842
+ ".xlsx",
4843
+ ".ppt",
4844
+ ".pptx",
4845
+ // Audio/Video
4846
+ ".mp3",
4847
+ ".mp4",
4848
+ ".wav",
4849
+ ".ogg",
4850
+ ".webm",
4851
+ ".mov",
4852
+ ".avi",
4853
+ ".mkv",
4854
+ // Archives
4855
+ ".zip",
4856
+ ".tar",
4857
+ ".gz",
4858
+ ".rar",
4859
+ ".7z",
4860
+ // Temp/System
4861
+ ".tmp",
4862
+ ".temp",
4863
+ ".swp",
4864
+ ".swo",
4865
+ ".bak",
4866
+ // Data
4867
+ ".json",
4868
+ ".yaml",
4869
+ ".yml",
4870
+ ".xml",
4871
+ ".csv",
4872
+ // Code (not markdown)
4873
+ ".js",
4874
+ ".ts",
4875
+ ".py",
4876
+ ".rb",
4877
+ ".go",
4878
+ ".rs",
4879
+ ".java",
4880
+ ".c",
4881
+ ".cpp",
4882
+ ".h",
4883
+ // Other
4884
+ ".exe",
4885
+ ".dll",
4886
+ ".so",
4887
+ ".dylib"
4888
+ ]);
4889
+ var IGNORED_PATTERNS = [
4890
+ /^\.#/,
4891
+ // Emacs lock files (.#filename)
4892
+ /~$/,
4893
+ // Backup files (filename~)
4894
+ /^#.*#$/,
4895
+ // Emacs auto-save (#filename#)
4896
+ /\.orig$/,
4897
+ // Merge conflict originals
4898
+ /\.swp$/,
4899
+ // Vim swap files
4900
+ /\.DS_Store$/,
4901
+ // macOS metadata
4902
+ /^Thumbs\.db$/i,
4903
+ // Windows thumbnail cache
4904
+ /^desktop\.ini$/i
4905
+ // Windows folder settings
4906
+ ];
4907
+ function isIgnoredDirectory(segment) {
4908
+ return IGNORED_DIRECTORIES.has(segment);
4909
+ }
4910
+ function hasIgnoredExtension(filePath) {
4911
+ const ext = path11.extname(filePath).toLowerCase();
4912
+ return IGNORED_EXTENSIONS.has(ext);
4913
+ }
4914
+ function matchesIgnoredPattern(filename) {
4915
+ return IGNORED_PATTERNS.some((pattern) => pattern.test(filename));
4916
+ }
4917
+ function normalizePath(filePath) {
4918
+ let normalized = filePath.replace(/\\/g, "/");
4919
+ if (process.platform === "win32") {
4920
+ normalized = normalized.toLowerCase();
4921
+ }
4922
+ return normalized;
4923
+ }
4924
+ function getRelativePath(vaultPath2, filePath) {
4925
+ const relative = path11.relative(vaultPath2, filePath);
4926
+ return normalizePath(relative);
4927
+ }
4928
+ function shouldWatch(filePath, vaultPath2) {
4929
+ const normalized = normalizePath(filePath);
4930
+ const relativePath = vaultPath2 ? getRelativePath(vaultPath2, filePath) : normalized;
4931
+ const segments = relativePath.split("/").filter((s) => s.length > 0);
4932
+ if (segments.length === 0) {
4933
+ return false;
4934
+ }
4935
+ for (const segment of segments.slice(0, -1)) {
4936
+ if (isIgnoredDirectory(segment)) {
4937
+ return false;
4938
+ }
4939
+ }
4940
+ const filename = segments[segments.length - 1];
4941
+ if (!filename.toLowerCase().endsWith(".md")) {
4942
+ return false;
4943
+ }
4944
+ if (hasIgnoredExtension(filename)) {
4945
+ return false;
4946
+ }
4947
+ if (matchesIgnoredPattern(filename)) {
4948
+ return false;
4949
+ }
4950
+ if (filename.startsWith(".")) {
4951
+ return false;
4952
+ }
4953
+ return true;
4954
+ }
4955
+ function createIgnoreFunction(vaultPath2) {
4956
+ return (filePath) => {
4957
+ return !shouldWatch(filePath, vaultPath2);
4958
+ };
4959
+ }
4960
+
4961
+ // src/core/watch/eventQueue.ts
4962
+ function coalesceEvents(events) {
4963
+ if (events.length === 0) return null;
4964
+ const types = events.map((e) => e.type);
4965
+ const lastUnlink = types.lastIndexOf("unlink");
4966
+ const lastAdd = types.lastIndexOf("add");
4967
+ const lastChange = types.lastIndexOf("change");
4968
+ if (lastUnlink > lastAdd && lastUnlink > lastChange) {
4969
+ return "delete";
4970
+ }
4971
+ if (lastAdd >= 0 || lastChange >= 0) {
4972
+ return "upsert";
4973
+ }
4974
+ if (types.every((t) => t === "unlink")) {
4975
+ return "delete";
4976
+ }
4977
+ return null;
4978
+ }
4979
+ var EventQueue = class {
4980
+ pending = /* @__PURE__ */ new Map();
4981
+ config;
4982
+ flushTimer = null;
4983
+ onBatch;
4984
+ constructor(config, onBatch) {
4985
+ this.config = config;
4986
+ this.onBatch = onBatch;
4987
+ }
4988
+ /**
4989
+ * Add a new event to the queue
4990
+ */
4991
+ push(type, rawPath) {
4992
+ const path12 = normalizePath(rawPath);
4993
+ const now = Date.now();
4994
+ const event = {
4995
+ type,
4996
+ path: path12,
4997
+ timestamp: now
4998
+ };
4999
+ let pending = this.pending.get(path12);
5000
+ if (!pending) {
5001
+ pending = {
5002
+ events: [],
5003
+ timer: null,
5004
+ lastEvent: now
5005
+ };
5006
+ this.pending.set(path12, pending);
5007
+ }
5008
+ pending.events.push(event);
5009
+ pending.lastEvent = now;
5010
+ if (pending.timer) {
5011
+ clearTimeout(pending.timer);
5012
+ }
5013
+ pending.timer = setTimeout(() => {
5014
+ this.flushPath(path12);
5015
+ }, this.config.debounceMs);
5016
+ if (this.pending.size >= this.config.batchSize) {
5017
+ this.flush();
5018
+ return;
5019
+ }
5020
+ this.ensureFlushTimer();
5021
+ }
5022
+ /**
5023
+ * Ensure the global flush timer is running
5024
+ */
5025
+ ensureFlushTimer() {
5026
+ if (this.flushTimer) return;
5027
+ this.flushTimer = setTimeout(() => {
5028
+ this.flushTimer = null;
5029
+ this.flush();
5030
+ }, this.config.flushMs);
5031
+ }
5032
+ /**
5033
+ * Flush a single path's events
5034
+ */
5035
+ flushPath(path12) {
5036
+ const pending = this.pending.get(path12);
5037
+ if (!pending || pending.events.length === 0) return;
5038
+ if (pending.timer) {
5039
+ clearTimeout(pending.timer);
5040
+ pending.timer = null;
5041
+ }
5042
+ const coalescedType = coalesceEvents(pending.events);
5043
+ if (coalescedType) {
5044
+ const coalesced = {
5045
+ type: coalescedType,
5046
+ path: path12,
5047
+ originalEvents: [...pending.events]
5048
+ };
5049
+ this.onBatch({
5050
+ events: [coalesced],
5051
+ timestamp: Date.now()
5052
+ });
5053
+ }
5054
+ this.pending.delete(path12);
5055
+ }
5056
+ /**
5057
+ * Flush all pending events
5058
+ */
5059
+ flush() {
5060
+ if (this.flushTimer) {
5061
+ clearTimeout(this.flushTimer);
5062
+ this.flushTimer = null;
5063
+ }
5064
+ if (this.pending.size === 0) return;
5065
+ const events = [];
5066
+ for (const [path12, pending] of this.pending) {
5067
+ if (pending.timer) {
5068
+ clearTimeout(pending.timer);
5069
+ }
5070
+ const coalescedType = coalesceEvents(pending.events);
5071
+ if (coalescedType) {
5072
+ events.push({
5073
+ type: coalescedType,
5074
+ path: path12,
5075
+ originalEvents: [...pending.events]
5076
+ });
5077
+ }
5078
+ }
5079
+ this.pending.clear();
5080
+ if (events.length > 0) {
5081
+ this.onBatch({
5082
+ events,
5083
+ timestamp: Date.now()
5084
+ });
5085
+ }
5086
+ }
5087
+ /**
5088
+ * Get the number of pending paths
5089
+ */
5090
+ get size() {
5091
+ return this.pending.size;
5092
+ }
5093
+ /**
5094
+ * Get the total number of pending events
5095
+ */
5096
+ get eventCount() {
5097
+ let count = 0;
5098
+ for (const pending of this.pending.values()) {
5099
+ count += pending.events.length;
5100
+ }
5101
+ return count;
5102
+ }
5103
+ /**
5104
+ * Clear all pending events without processing
5105
+ */
5106
+ clear() {
5107
+ for (const pending of this.pending.values()) {
5108
+ if (pending.timer) {
5109
+ clearTimeout(pending.timer);
5110
+ }
5111
+ }
5112
+ if (this.flushTimer) {
5113
+ clearTimeout(this.flushTimer);
5114
+ this.flushTimer = null;
5115
+ }
5116
+ this.pending.clear();
5117
+ }
5118
+ /**
5119
+ * Dispose the queue
5120
+ */
5121
+ dispose() {
5122
+ this.clear();
5123
+ }
5124
+ };
5125
+
5126
+ // src/core/watch/types.ts
5127
+ var DEFAULT_WATCHER_CONFIG = {
5128
+ debounceMs: 200,
5129
+ flushMs: 1e3,
5130
+ batchSize: 50,
5131
+ usePolling: false,
5132
+ pollInterval: 500
5133
+ };
5134
+ function parseWatcherConfig() {
5135
+ const debounceMs = parseInt(process.env.FLYWHEEL_DEBOUNCE_MS || "");
5136
+ const flushMs = parseInt(process.env.FLYWHEEL_FLUSH_MS || "");
5137
+ const batchSize = parseInt(process.env.FLYWHEEL_BATCH_SIZE || "");
5138
+ const usePolling = process.env.FLYWHEEL_WATCH_POLL === "true";
5139
+ const pollInterval = parseInt(process.env.FLYWHEEL_POLL_INTERVAL || "");
5140
+ return {
5141
+ debounceMs: Number.isFinite(debounceMs) && debounceMs > 0 ? debounceMs : DEFAULT_WATCHER_CONFIG.debounceMs,
5142
+ flushMs: Number.isFinite(flushMs) && flushMs > 0 ? flushMs : DEFAULT_WATCHER_CONFIG.flushMs,
5143
+ batchSize: Number.isFinite(batchSize) && batchSize > 0 ? batchSize : DEFAULT_WATCHER_CONFIG.batchSize,
5144
+ usePolling,
5145
+ pollInterval: Number.isFinite(pollInterval) && pollInterval > 0 ? pollInterval : DEFAULT_WATCHER_CONFIG.pollInterval
5146
+ };
5147
+ }
5148
+
5149
+ // src/core/watch/index.ts
5150
+ function createVaultWatcher(options) {
5151
+ const { vaultPath: vaultPath2, onBatch, onStateChange, onError } = options;
5152
+ const config = {
5153
+ ...DEFAULT_WATCHER_CONFIG,
5154
+ ...parseWatcherConfig(),
5155
+ ...options.config
5156
+ };
5157
+ let state = "starting";
5158
+ let lastRebuild = null;
5159
+ let error = null;
5160
+ let watcher = null;
5161
+ let processingBatch = false;
5162
+ let pendingBatches = [];
5163
+ const getStatus = () => ({
5164
+ state,
5165
+ pendingEvents: eventQueue?.eventCount || 0,
5166
+ lastRebuild,
5167
+ error
5168
+ });
5169
+ const setState = (newState, newError = null) => {
5170
+ state = newState;
5171
+ error = newError;
5172
+ onStateChange?.(getStatus());
5173
+ };
5174
+ const processBatch2 = async (batch) => {
5175
+ if (processingBatch) {
5176
+ pendingBatches.push(batch);
5177
+ return;
5178
+ }
5179
+ processingBatch = true;
5180
+ setState("rebuilding");
5181
+ try {
5182
+ await onBatch(batch);
5183
+ lastRebuild = Date.now();
5184
+ setState("ready");
5185
+ } catch (err) {
5186
+ const e = err instanceof Error ? err : new Error(String(err));
5187
+ setState("error", e);
5188
+ onError?.(e);
5189
+ } finally {
5190
+ processingBatch = false;
5191
+ if (pendingBatches.length > 0) {
5192
+ const nextBatch = pendingBatches.shift();
5193
+ setImmediate(() => processBatch2(nextBatch));
5194
+ }
5195
+ }
5196
+ };
5197
+ const eventQueue = new EventQueue(config, processBatch2);
5198
+ const instance = {
5199
+ get status() {
5200
+ return getStatus();
5201
+ },
5202
+ get pendingCount() {
5203
+ return eventQueue.eventCount + pendingBatches.reduce((sum, b) => sum + b.events.length, 0);
5204
+ },
5205
+ start() {
5206
+ if (watcher) {
5207
+ console.error("[flywheel] Watcher already started");
5208
+ return;
5209
+ }
5210
+ console.error(`[flywheel] Starting file watcher (debounce: ${config.debounceMs}ms, flush: ${config.flushMs}ms)`);
5211
+ watcher = chokidar.watch(vaultPath2, {
5212
+ ignored: createIgnoreFunction(vaultPath2),
5213
+ persistent: true,
5214
+ ignoreInitial: true,
5215
+ awaitWriteFinish: {
5216
+ stabilityThreshold: 300,
5217
+ pollInterval: 100
5218
+ },
5219
+ usePolling: config.usePolling,
5220
+ interval: config.usePolling ? config.pollInterval : void 0
5221
+ });
5222
+ watcher.on("add", (path12) => {
5223
+ if (shouldWatch(path12, vaultPath2)) {
5224
+ eventQueue.push("add", path12);
5225
+ }
5226
+ });
5227
+ watcher.on("change", (path12) => {
5228
+ if (shouldWatch(path12, vaultPath2)) {
5229
+ eventQueue.push("change", path12);
5230
+ }
5231
+ });
5232
+ watcher.on("unlink", (path12) => {
5233
+ if (shouldWatch(path12, vaultPath2)) {
5234
+ eventQueue.push("unlink", path12);
5235
+ }
5236
+ });
5237
+ watcher.on("ready", () => {
5238
+ console.error("[flywheel] File watcher ready");
5239
+ setState("ready");
5240
+ });
5241
+ watcher.on("error", (err) => {
5242
+ console.error("[flywheel] Watcher error:", err);
5243
+ const e = err instanceof Error ? err : new Error(String(err));
5244
+ setState("error", e);
5245
+ onError?.(e);
5246
+ });
5247
+ },
5248
+ stop() {
5249
+ if (!watcher) {
5250
+ return;
5251
+ }
5252
+ console.error("[flywheel] Stopping file watcher");
5253
+ eventQueue.flush();
5254
+ watcher.close();
5255
+ watcher = null;
5256
+ eventQueue.dispose();
5257
+ },
5258
+ flush() {
5259
+ eventQueue.flush();
5260
+ }
5261
+ };
5262
+ return instance;
5263
+ }
5264
+
4809
5265
  // src/index.ts
4810
5266
  var vaultPath = process.env.PROJECT_PATH || findVaultRoot();
4811
5267
  var flywheelConfig = {};
@@ -4930,35 +5386,66 @@ async function main() {
4930
5386
  console.error(`[Flywheel] Excluding task tags: ${flywheelConfig.exclude_task_tags.join(", ")}`);
4931
5387
  }
4932
5388
  if (process.env.FLYWHEEL_WATCH === "true") {
4933
- const debounceMs = parseInt(process.env.FLYWHEEL_DEBOUNCE_MS || "60000");
4934
- console.error(`[flywheel] File watcher enabled (debounce: ${debounceMs}ms)`);
4935
- const watcher = chokidar.watch(vaultPath, {
4936
- ignored: /(^|[\/\\])\../,
4937
- // ignore dotfiles
4938
- persistent: true,
4939
- ignoreInitial: true,
4940
- awaitWriteFinish: {
4941
- stabilityThreshold: 300,
4942
- pollInterval: 100
4943
- }
4944
- });
4945
- let rebuildTimer;
4946
- watcher.on("all", (event, path11) => {
4947
- if (!path11.endsWith(".md")) return;
4948
- clearTimeout(rebuildTimer);
4949
- rebuildTimer = setTimeout(() => {
4950
- console.error("[flywheel] Rebuilding index (file changed)");
4951
- buildVaultIndex(vaultPath).then((index2) => {
4952
- vaultIndex = index2;
4953
- setIndexState("ready");
4954
- console.error("[flywheel] Index rebuilt successfully");
4955
- }).catch((err) => {
4956
- setIndexState("error");
4957
- setIndexError(err instanceof Error ? err : new Error(String(err)));
4958
- console.error("[flywheel] Failed to rebuild index:", err);
4959
- });
4960
- }, debounceMs);
4961
- });
5389
+ if (process.env.FLYWHEEL_WATCH_V2 === "true") {
5390
+ const config = parseWatcherConfig();
5391
+ console.error(`[flywheel] File watcher v2 enabled (debounce: ${config.debounceMs}ms, flush: ${config.flushMs}ms)`);
5392
+ const watcher = createVaultWatcher({
5393
+ vaultPath,
5394
+ config,
5395
+ onBatch: async (batch) => {
5396
+ console.error(`[flywheel] Processing ${batch.events.length} file changes`);
5397
+ const startTime2 = Date.now();
5398
+ try {
5399
+ vaultIndex = await buildVaultIndex(vaultPath);
5400
+ setIndexState("ready");
5401
+ console.error(`[flywheel] Index rebuilt in ${Date.now() - startTime2}ms`);
5402
+ } catch (err) {
5403
+ setIndexState("error");
5404
+ setIndexError(err instanceof Error ? err : new Error(String(err)));
5405
+ console.error("[flywheel] Failed to rebuild index:", err);
5406
+ }
5407
+ },
5408
+ onStateChange: (status) => {
5409
+ if (status.state === "dirty") {
5410
+ console.error("[flywheel] Warning: Index may be stale");
5411
+ }
5412
+ },
5413
+ onError: (err) => {
5414
+ console.error("[flywheel] Watcher error:", err.message);
5415
+ }
5416
+ });
5417
+ watcher.start();
5418
+ } else {
5419
+ const debounceMs = parseInt(process.env.FLYWHEEL_DEBOUNCE_MS || "60000");
5420
+ console.error(`[flywheel] File watcher v1 enabled (debounce: ${debounceMs}ms)`);
5421
+ const legacyWatcher = chokidar2.watch(vaultPath, {
5422
+ ignored: /(^|[\/\\])\../,
5423
+ // ignore dotfiles
5424
+ persistent: true,
5425
+ ignoreInitial: true,
5426
+ awaitWriteFinish: {
5427
+ stabilityThreshold: 300,
5428
+ pollInterval: 100
5429
+ }
5430
+ });
5431
+ let rebuildTimer;
5432
+ legacyWatcher.on("all", (event, path12) => {
5433
+ if (!path12.endsWith(".md")) return;
5434
+ clearTimeout(rebuildTimer);
5435
+ rebuildTimer = setTimeout(() => {
5436
+ console.error("[flywheel] Rebuilding index (file changed)");
5437
+ buildVaultIndex(vaultPath).then((index2) => {
5438
+ vaultIndex = index2;
5439
+ setIndexState("ready");
5440
+ console.error("[flywheel] Index rebuilt successfully");
5441
+ }).catch((err) => {
5442
+ setIndexState("error");
5443
+ setIndexError(err instanceof Error ? err : new Error(String(err)));
5444
+ console.error("[flywheel] Failed to rebuild index:", err);
5445
+ });
5446
+ }, debounceMs);
5447
+ });
5448
+ }
4962
5449
  }
4963
5450
  }).catch((err) => {
4964
5451
  setIndexState("error");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-mcp",
3
- "version": "1.25.5",
3
+ "version": "1.26.2",
4
4
  "description": "Query your markdown like a database. 100x token savings for AI agents.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",