@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.
- package/dist/index.js +551 -64
- 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
|
|
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(
|
|
264
|
-
return
|
|
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,
|
|
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:
|
|
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,
|
|
892
|
+
for (const [name, path12] of entities) {
|
|
893
893
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
894
|
-
return
|
|
894
|
+
return path12;
|
|
895
895
|
}
|
|
896
896
|
}
|
|
897
|
-
for (const [name,
|
|
897
|
+
for (const [name, path12] of entities) {
|
|
898
898
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
899
|
-
return
|
|
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(
|
|
1197
|
-
const filename =
|
|
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 =
|
|
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,
|
|
2135
|
-
const targetNote = index.notes.get(
|
|
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 ===
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
2980
|
+
async ({ path: path12 }) => {
|
|
2981
2981
|
const index = getIndex();
|
|
2982
2982
|
const vaultPath2 = getVaultPath();
|
|
2983
|
-
const result = await getNoteStructure(index,
|
|
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:
|
|
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:
|
|
3003
|
+
async ({ path: path12 }) => {
|
|
3004
3004
|
const index = getIndex();
|
|
3005
3005
|
const vaultPath2 = getVaultPath();
|
|
3006
|
-
const result = await getHeadings(index,
|
|
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:
|
|
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:
|
|
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:
|
|
3032
|
+
async ({ path: path12, heading, include_subheadings }) => {
|
|
3033
3033
|
const index = getIndex();
|
|
3034
3034
|
const vaultPath2 = getVaultPath();
|
|
3035
|
-
const result = await getSectionContent(index,
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
console.error("[flywheel]
|
|
4959
|
-
}
|
|
4960
|
-
}
|
|
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");
|