@velvetmonkey/flywheel-memory 2.0.36 → 2.0.37
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 +775 -448
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ var init_constants = __esm({
|
|
|
59
59
|
|
|
60
60
|
// src/core/write/writer.ts
|
|
61
61
|
import fs18 from "fs/promises";
|
|
62
|
-
import
|
|
62
|
+
import path20 from "path";
|
|
63
63
|
import matter5 from "gray-matter";
|
|
64
64
|
function isSensitivePath(filePath) {
|
|
65
65
|
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
@@ -386,8 +386,8 @@ function validatePath(vaultPath2, notePath) {
|
|
|
386
386
|
if (notePath.startsWith("\\")) {
|
|
387
387
|
return false;
|
|
388
388
|
}
|
|
389
|
-
const resolvedVault =
|
|
390
|
-
const resolvedNote =
|
|
389
|
+
const resolvedVault = path20.resolve(vaultPath2);
|
|
390
|
+
const resolvedNote = path20.resolve(vaultPath2, notePath);
|
|
391
391
|
return resolvedNote.startsWith(resolvedVault);
|
|
392
392
|
}
|
|
393
393
|
async function validatePathSecure(vaultPath2, notePath) {
|
|
@@ -415,8 +415,8 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
415
415
|
reason: "Path traversal not allowed"
|
|
416
416
|
};
|
|
417
417
|
}
|
|
418
|
-
const resolvedVault =
|
|
419
|
-
const resolvedNote =
|
|
418
|
+
const resolvedVault = path20.resolve(vaultPath2);
|
|
419
|
+
const resolvedNote = path20.resolve(vaultPath2, notePath);
|
|
420
420
|
if (!resolvedNote.startsWith(resolvedVault)) {
|
|
421
421
|
return {
|
|
422
422
|
valid: false,
|
|
@@ -430,7 +430,7 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
430
430
|
};
|
|
431
431
|
}
|
|
432
432
|
try {
|
|
433
|
-
const fullPath =
|
|
433
|
+
const fullPath = path20.join(vaultPath2, notePath);
|
|
434
434
|
try {
|
|
435
435
|
await fs18.access(fullPath);
|
|
436
436
|
const realPath = await fs18.realpath(fullPath);
|
|
@@ -441,7 +441,7 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
441
441
|
reason: "Symlink target is outside vault"
|
|
442
442
|
};
|
|
443
443
|
}
|
|
444
|
-
const relativePath =
|
|
444
|
+
const relativePath = path20.relative(realVaultPath, realPath);
|
|
445
445
|
if (isSensitivePath(relativePath)) {
|
|
446
446
|
return {
|
|
447
447
|
valid: false,
|
|
@@ -449,7 +449,7 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
449
449
|
};
|
|
450
450
|
}
|
|
451
451
|
} catch {
|
|
452
|
-
const parentDir =
|
|
452
|
+
const parentDir = path20.dirname(fullPath);
|
|
453
453
|
try {
|
|
454
454
|
await fs18.access(parentDir);
|
|
455
455
|
const realParentPath = await fs18.realpath(parentDir);
|
|
@@ -475,8 +475,8 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
475
475
|
if (!validatePath(vaultPath2, notePath)) {
|
|
476
476
|
throw new Error("Invalid path: path traversal not allowed");
|
|
477
477
|
}
|
|
478
|
-
const fullPath =
|
|
479
|
-
const [rawContent,
|
|
478
|
+
const fullPath = path20.join(vaultPath2, notePath);
|
|
479
|
+
const [rawContent, stat4] = await Promise.all([
|
|
480
480
|
fs18.readFile(fullPath, "utf-8"),
|
|
481
481
|
fs18.stat(fullPath)
|
|
482
482
|
]);
|
|
@@ -489,7 +489,7 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
489
489
|
frontmatter,
|
|
490
490
|
rawContent,
|
|
491
491
|
lineEnding,
|
|
492
|
-
mtimeMs:
|
|
492
|
+
mtimeMs: stat4.mtimeMs
|
|
493
493
|
};
|
|
494
494
|
}
|
|
495
495
|
function deepCloneFrontmatter(obj) {
|
|
@@ -528,7 +528,7 @@ async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEn
|
|
|
528
528
|
if (!validation.valid) {
|
|
529
529
|
throw new Error(`Invalid path: ${validation.reason}`);
|
|
530
530
|
}
|
|
531
|
-
const fullPath =
|
|
531
|
+
const fullPath = path20.join(vaultPath2, notePath);
|
|
532
532
|
let output = matter5.stringify(content, frontmatter);
|
|
533
533
|
output = normalizeTrailingNewline(output);
|
|
534
534
|
output = convertLineEndings(output, lineEnding);
|
|
@@ -836,8 +836,8 @@ function createContext(variables = {}) {
|
|
|
836
836
|
steps: {}
|
|
837
837
|
};
|
|
838
838
|
}
|
|
839
|
-
function resolvePath(obj,
|
|
840
|
-
const parts =
|
|
839
|
+
function resolvePath(obj, path32) {
|
|
840
|
+
const parts = path32.split(".");
|
|
841
841
|
let current = obj;
|
|
842
842
|
for (const part of parts) {
|
|
843
843
|
if (current === void 0 || current === null) {
|
|
@@ -1279,7 +1279,7 @@ __export(conditions_exports, {
|
|
|
1279
1279
|
shouldStepExecute: () => shouldStepExecute
|
|
1280
1280
|
});
|
|
1281
1281
|
import fs25 from "fs/promises";
|
|
1282
|
-
import
|
|
1282
|
+
import path26 from "path";
|
|
1283
1283
|
async function evaluateCondition(condition, vaultPath2, context) {
|
|
1284
1284
|
const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
|
|
1285
1285
|
const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
|
|
@@ -1332,7 +1332,7 @@ async function evaluateCondition(condition, vaultPath2, context) {
|
|
|
1332
1332
|
}
|
|
1333
1333
|
}
|
|
1334
1334
|
async function evaluateFileExists(vaultPath2, notePath, expectExists) {
|
|
1335
|
-
const fullPath =
|
|
1335
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
1336
1336
|
try {
|
|
1337
1337
|
await fs25.access(fullPath);
|
|
1338
1338
|
return {
|
|
@@ -1347,7 +1347,7 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
|
|
|
1347
1347
|
}
|
|
1348
1348
|
}
|
|
1349
1349
|
async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
|
|
1350
|
-
const fullPath =
|
|
1350
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
1351
1351
|
try {
|
|
1352
1352
|
await fs25.access(fullPath);
|
|
1353
1353
|
} catch {
|
|
@@ -1378,7 +1378,7 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
|
|
|
1378
1378
|
}
|
|
1379
1379
|
}
|
|
1380
1380
|
async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
|
|
1381
|
-
const fullPath =
|
|
1381
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
1382
1382
|
try {
|
|
1383
1383
|
await fs25.access(fullPath);
|
|
1384
1384
|
} catch {
|
|
@@ -1409,7 +1409,7 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
|
|
|
1409
1409
|
}
|
|
1410
1410
|
}
|
|
1411
1411
|
async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
|
|
1412
|
-
const fullPath =
|
|
1412
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
1413
1413
|
try {
|
|
1414
1414
|
await fs25.access(fullPath);
|
|
1415
1415
|
} catch {
|
|
@@ -1552,7 +1552,7 @@ var init_taskHelpers = __esm({
|
|
|
1552
1552
|
});
|
|
1553
1553
|
|
|
1554
1554
|
// src/index.ts
|
|
1555
|
-
import * as
|
|
1555
|
+
import * as path31 from "path";
|
|
1556
1556
|
import { readFileSync as readFileSync4, realpathSync } from "fs";
|
|
1557
1557
|
import { fileURLToPath } from "url";
|
|
1558
1558
|
import { dirname as dirname4, join as join16 } from "path";
|
|
@@ -2216,8 +2216,8 @@ function updateIndexProgress(parsed, total) {
|
|
|
2216
2216
|
function normalizeTarget(target) {
|
|
2217
2217
|
return target.toLowerCase().replace(/\.md$/, "");
|
|
2218
2218
|
}
|
|
2219
|
-
function normalizeNotePath(
|
|
2220
|
-
return
|
|
2219
|
+
function normalizeNotePath(path32) {
|
|
2220
|
+
return path32.toLowerCase().replace(/\.md$/, "");
|
|
2221
2221
|
}
|
|
2222
2222
|
async function buildVaultIndex(vaultPath2, options = {}) {
|
|
2223
2223
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
|
|
@@ -2386,7 +2386,7 @@ function findSimilarEntity(index, target) {
|
|
|
2386
2386
|
}
|
|
2387
2387
|
const maxDist = normalizedLen <= 10 ? 1 : 2;
|
|
2388
2388
|
let bestMatch;
|
|
2389
|
-
for (const [entity,
|
|
2389
|
+
for (const [entity, path32] of index.entities) {
|
|
2390
2390
|
const lenDiff = Math.abs(entity.length - normalizedLen);
|
|
2391
2391
|
if (lenDiff > maxDist) {
|
|
2392
2392
|
continue;
|
|
@@ -2394,7 +2394,7 @@ function findSimilarEntity(index, target) {
|
|
|
2394
2394
|
const dist = levenshteinDistance(normalized, entity);
|
|
2395
2395
|
if (dist > 0 && dist <= maxDist) {
|
|
2396
2396
|
if (!bestMatch || dist < bestMatch.distance) {
|
|
2397
|
-
bestMatch = { path:
|
|
2397
|
+
bestMatch = { path: path32, entity, distance: dist };
|
|
2398
2398
|
if (dist === 1) {
|
|
2399
2399
|
return bestMatch;
|
|
2400
2400
|
}
|
|
@@ -2667,6 +2667,9 @@ function findVaultRoot(startPath) {
|
|
|
2667
2667
|
// src/core/read/watch/index.ts
|
|
2668
2668
|
import chokidar from "chokidar";
|
|
2669
2669
|
|
|
2670
|
+
// src/core/read/watch/eventQueue.ts
|
|
2671
|
+
import * as path6 from "path";
|
|
2672
|
+
|
|
2670
2673
|
// src/core/read/watch/pathFilter.ts
|
|
2671
2674
|
import path5 from "path";
|
|
2672
2675
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -2780,8 +2783,8 @@ function normalizePath(filePath) {
|
|
|
2780
2783
|
return normalized;
|
|
2781
2784
|
}
|
|
2782
2785
|
function getRelativePath(vaultPath2, filePath) {
|
|
2783
|
-
const
|
|
2784
|
-
return normalizePath(
|
|
2786
|
+
const relative2 = path5.relative(vaultPath2, filePath);
|
|
2787
|
+
return normalizePath(relative2);
|
|
2785
2788
|
}
|
|
2786
2789
|
function shouldWatch(filePath, vaultPath2) {
|
|
2787
2790
|
const normalized = normalizePath(filePath);
|
|
@@ -2851,6 +2854,52 @@ function coalesceEvents(events) {
|
|
|
2851
2854
|
}
|
|
2852
2855
|
return null;
|
|
2853
2856
|
}
|
|
2857
|
+
var RENAME_PROXIMITY_MS = 5e3;
|
|
2858
|
+
function fileStem(p) {
|
|
2859
|
+
return path6.basename(p).replace(/\.[^.]+$/, "");
|
|
2860
|
+
}
|
|
2861
|
+
function detectRenames(events) {
|
|
2862
|
+
const deletes = events.filter((e) => e.type === "delete");
|
|
2863
|
+
const upserts = events.filter((e) => e.type === "upsert");
|
|
2864
|
+
const others = events.filter((e) => e.type !== "delete" && e.type !== "upsert");
|
|
2865
|
+
const usedDeletes = /* @__PURE__ */ new Set();
|
|
2866
|
+
const usedUpserts = /* @__PURE__ */ new Set();
|
|
2867
|
+
const renames = [];
|
|
2868
|
+
for (const del of deletes) {
|
|
2869
|
+
const stem2 = fileStem(del.path);
|
|
2870
|
+
const delTimestamp = del.originalEvents.length > 0 ? Math.max(...del.originalEvents.map((e) => e.timestamp)) : 0;
|
|
2871
|
+
const candidates = upserts.filter(
|
|
2872
|
+
(u) => !usedUpserts.has(u.path) && fileStem(u.path) === stem2
|
|
2873
|
+
);
|
|
2874
|
+
if (candidates.length === 0) continue;
|
|
2875
|
+
let bestCandidate = null;
|
|
2876
|
+
let bestDelta = Infinity;
|
|
2877
|
+
for (const candidate of candidates) {
|
|
2878
|
+
const addTimestamp = candidate.originalEvents.length > 0 ? Math.max(...candidate.originalEvents.map((e) => e.timestamp)) : 0;
|
|
2879
|
+
const delta = Math.abs(addTimestamp - delTimestamp);
|
|
2880
|
+
if (delta <= RENAME_PROXIMITY_MS && delta < bestDelta) {
|
|
2881
|
+
bestDelta = delta;
|
|
2882
|
+
bestCandidate = candidate;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
if (bestCandidate) {
|
|
2886
|
+
usedDeletes.add(del.path);
|
|
2887
|
+
usedUpserts.add(bestCandidate.path);
|
|
2888
|
+
renames.push({
|
|
2889
|
+
type: "rename",
|
|
2890
|
+
oldPath: del.path,
|
|
2891
|
+
newPath: bestCandidate.path,
|
|
2892
|
+
timestamp: Date.now()
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
const nonRenameEvents = [
|
|
2897
|
+
...deletes.filter((e) => !usedDeletes.has(e.path)),
|
|
2898
|
+
...upserts.filter((e) => !usedUpserts.has(e.path)),
|
|
2899
|
+
...others
|
|
2900
|
+
];
|
|
2901
|
+
return { nonRenameEvents, renames };
|
|
2902
|
+
}
|
|
2854
2903
|
var EventQueue = class {
|
|
2855
2904
|
pending = /* @__PURE__ */ new Map();
|
|
2856
2905
|
config;
|
|
@@ -2864,30 +2913,30 @@ var EventQueue = class {
|
|
|
2864
2913
|
* Add a new event to the queue
|
|
2865
2914
|
*/
|
|
2866
2915
|
push(type, rawPath) {
|
|
2867
|
-
const
|
|
2916
|
+
const path32 = normalizePath(rawPath);
|
|
2868
2917
|
const now = Date.now();
|
|
2869
2918
|
const event = {
|
|
2870
2919
|
type,
|
|
2871
|
-
path:
|
|
2920
|
+
path: path32,
|
|
2872
2921
|
timestamp: now
|
|
2873
2922
|
};
|
|
2874
|
-
let pending = this.pending.get(
|
|
2923
|
+
let pending = this.pending.get(path32);
|
|
2875
2924
|
if (!pending) {
|
|
2876
2925
|
pending = {
|
|
2877
2926
|
events: [],
|
|
2878
2927
|
timer: null,
|
|
2879
2928
|
lastEvent: now
|
|
2880
2929
|
};
|
|
2881
|
-
this.pending.set(
|
|
2930
|
+
this.pending.set(path32, pending);
|
|
2882
2931
|
}
|
|
2883
2932
|
pending.events.push(event);
|
|
2884
2933
|
pending.lastEvent = now;
|
|
2885
|
-
console.error(`[flywheel] QUEUE: pushed ${type} for ${
|
|
2934
|
+
console.error(`[flywheel] QUEUE: pushed ${type} for ${path32}, pending=${this.pending.size}`);
|
|
2886
2935
|
if (pending.timer) {
|
|
2887
2936
|
clearTimeout(pending.timer);
|
|
2888
2937
|
}
|
|
2889
2938
|
pending.timer = setTimeout(() => {
|
|
2890
|
-
this.flushPath(
|
|
2939
|
+
this.flushPath(path32);
|
|
2891
2940
|
}, this.config.debounceMs);
|
|
2892
2941
|
if (this.pending.size >= this.config.batchSize) {
|
|
2893
2942
|
this.flush();
|
|
@@ -2908,10 +2957,10 @@ var EventQueue = class {
|
|
|
2908
2957
|
/**
|
|
2909
2958
|
* Flush a single path's events
|
|
2910
2959
|
*/
|
|
2911
|
-
flushPath(
|
|
2912
|
-
const pending = this.pending.get(
|
|
2960
|
+
flushPath(path32) {
|
|
2961
|
+
const pending = this.pending.get(path32);
|
|
2913
2962
|
if (!pending || pending.events.length === 0) return;
|
|
2914
|
-
console.error(`[flywheel] QUEUE: flushing ${
|
|
2963
|
+
console.error(`[flywheel] QUEUE: flushing ${path32}, events=${pending.events.length}`);
|
|
2915
2964
|
if (pending.timer) {
|
|
2916
2965
|
clearTimeout(pending.timer);
|
|
2917
2966
|
pending.timer = null;
|
|
@@ -2920,15 +2969,16 @@ var EventQueue = class {
|
|
|
2920
2969
|
if (coalescedType) {
|
|
2921
2970
|
const coalesced = {
|
|
2922
2971
|
type: coalescedType,
|
|
2923
|
-
path:
|
|
2972
|
+
path: path32,
|
|
2924
2973
|
originalEvents: [...pending.events]
|
|
2925
2974
|
};
|
|
2926
2975
|
this.onBatch({
|
|
2927
2976
|
events: [coalesced],
|
|
2977
|
+
renames: [],
|
|
2928
2978
|
timestamp: Date.now()
|
|
2929
2979
|
});
|
|
2930
2980
|
}
|
|
2931
|
-
this.pending.delete(
|
|
2981
|
+
this.pending.delete(path32);
|
|
2932
2982
|
}
|
|
2933
2983
|
/**
|
|
2934
2984
|
* Flush all pending events
|
|
@@ -2940,7 +2990,7 @@ var EventQueue = class {
|
|
|
2940
2990
|
}
|
|
2941
2991
|
if (this.pending.size === 0) return;
|
|
2942
2992
|
const events = [];
|
|
2943
|
-
for (const [
|
|
2993
|
+
for (const [path32, pending] of this.pending) {
|
|
2944
2994
|
if (pending.timer) {
|
|
2945
2995
|
clearTimeout(pending.timer);
|
|
2946
2996
|
}
|
|
@@ -2948,15 +2998,17 @@ var EventQueue = class {
|
|
|
2948
2998
|
if (coalescedType) {
|
|
2949
2999
|
events.push({
|
|
2950
3000
|
type: coalescedType,
|
|
2951
|
-
path:
|
|
3001
|
+
path: path32,
|
|
2952
3002
|
originalEvents: [...pending.events]
|
|
2953
3003
|
});
|
|
2954
3004
|
}
|
|
2955
3005
|
}
|
|
2956
3006
|
this.pending.clear();
|
|
2957
3007
|
if (events.length > 0) {
|
|
3008
|
+
const { nonRenameEvents, renames } = detectRenames(events);
|
|
2958
3009
|
this.onBatch({
|
|
2959
|
-
events,
|
|
3010
|
+
events: nonRenameEvents,
|
|
3011
|
+
renames,
|
|
2960
3012
|
timestamp: Date.now()
|
|
2961
3013
|
});
|
|
2962
3014
|
}
|
|
@@ -3024,7 +3076,7 @@ function parseWatcherConfig() {
|
|
|
3024
3076
|
}
|
|
3025
3077
|
|
|
3026
3078
|
// src/core/read/watch/incrementalIndex.ts
|
|
3027
|
-
import
|
|
3079
|
+
import path7 from "path";
|
|
3028
3080
|
function normalizeTarget2(target) {
|
|
3029
3081
|
return target.toLowerCase().replace(/\.md$/, "");
|
|
3030
3082
|
}
|
|
@@ -3115,7 +3167,7 @@ async function upsertNote(index, vaultPath2, notePath) {
|
|
|
3115
3167
|
if (existed) {
|
|
3116
3168
|
removeNoteFromIndex(index, notePath);
|
|
3117
3169
|
}
|
|
3118
|
-
const fullPath =
|
|
3170
|
+
const fullPath = path7.join(vaultPath2, notePath);
|
|
3119
3171
|
const fs31 = await import("fs/promises");
|
|
3120
3172
|
const stats = await fs31.stat(fullPath);
|
|
3121
3173
|
const vaultFile = {
|
|
@@ -3299,31 +3351,31 @@ function createVaultWatcher(options) {
|
|
|
3299
3351
|
usePolling: config.usePolling,
|
|
3300
3352
|
interval: config.usePolling ? config.pollInterval : void 0
|
|
3301
3353
|
});
|
|
3302
|
-
watcher.on("add", (
|
|
3303
|
-
console.error(`[flywheel] RAW EVENT: add ${
|
|
3304
|
-
if (shouldWatch(
|
|
3305
|
-
console.error(`[flywheel] ACCEPTED: add ${
|
|
3306
|
-
eventQueue.push("add",
|
|
3354
|
+
watcher.on("add", (path32) => {
|
|
3355
|
+
console.error(`[flywheel] RAW EVENT: add ${path32}`);
|
|
3356
|
+
if (shouldWatch(path32, vaultPath2)) {
|
|
3357
|
+
console.error(`[flywheel] ACCEPTED: add ${path32}`);
|
|
3358
|
+
eventQueue.push("add", path32);
|
|
3307
3359
|
} else {
|
|
3308
|
-
console.error(`[flywheel] FILTERED: add ${
|
|
3360
|
+
console.error(`[flywheel] FILTERED: add ${path32}`);
|
|
3309
3361
|
}
|
|
3310
3362
|
});
|
|
3311
|
-
watcher.on("change", (
|
|
3312
|
-
console.error(`[flywheel] RAW EVENT: change ${
|
|
3313
|
-
if (shouldWatch(
|
|
3314
|
-
console.error(`[flywheel] ACCEPTED: change ${
|
|
3315
|
-
eventQueue.push("change",
|
|
3363
|
+
watcher.on("change", (path32) => {
|
|
3364
|
+
console.error(`[flywheel] RAW EVENT: change ${path32}`);
|
|
3365
|
+
if (shouldWatch(path32, vaultPath2)) {
|
|
3366
|
+
console.error(`[flywheel] ACCEPTED: change ${path32}`);
|
|
3367
|
+
eventQueue.push("change", path32);
|
|
3316
3368
|
} else {
|
|
3317
|
-
console.error(`[flywheel] FILTERED: change ${
|
|
3369
|
+
console.error(`[flywheel] FILTERED: change ${path32}`);
|
|
3318
3370
|
}
|
|
3319
3371
|
});
|
|
3320
|
-
watcher.on("unlink", (
|
|
3321
|
-
console.error(`[flywheel] RAW EVENT: unlink ${
|
|
3322
|
-
if (shouldWatch(
|
|
3323
|
-
console.error(`[flywheel] ACCEPTED: unlink ${
|
|
3324
|
-
eventQueue.push("unlink",
|
|
3372
|
+
watcher.on("unlink", (path32) => {
|
|
3373
|
+
console.error(`[flywheel] RAW EVENT: unlink ${path32}`);
|
|
3374
|
+
if (shouldWatch(path32, vaultPath2)) {
|
|
3375
|
+
console.error(`[flywheel] ACCEPTED: unlink ${path32}`);
|
|
3376
|
+
eventQueue.push("unlink", path32);
|
|
3325
3377
|
} else {
|
|
3326
|
-
console.error(`[flywheel] FILTERED: unlink ${
|
|
3378
|
+
console.error(`[flywheel] FILTERED: unlink ${path32}`);
|
|
3327
3379
|
}
|
|
3328
3380
|
});
|
|
3329
3381
|
watcher.on("ready", () => {
|
|
@@ -3527,13 +3579,13 @@ function updateSuppressionList(stateDb2) {
|
|
|
3527
3579
|
"DELETE FROM wikilink_suppressions WHERE entity = ?"
|
|
3528
3580
|
);
|
|
3529
3581
|
const transaction = stateDb2.db.transaction(() => {
|
|
3530
|
-
for (const
|
|
3531
|
-
const fpRate =
|
|
3582
|
+
for (const stat4 of stats) {
|
|
3583
|
+
const fpRate = stat4.false_positives / stat4.total;
|
|
3532
3584
|
if (fpRate >= SUPPRESSION_THRESHOLD) {
|
|
3533
|
-
upsert.run(
|
|
3585
|
+
upsert.run(stat4.entity, fpRate);
|
|
3534
3586
|
updated++;
|
|
3535
3587
|
} else {
|
|
3536
|
-
remove.run(
|
|
3588
|
+
remove.run(stat4.entity);
|
|
3537
3589
|
}
|
|
3538
3590
|
}
|
|
3539
3591
|
});
|
|
@@ -3660,6 +3712,46 @@ function getTrackedApplications(stateDb2, notePath) {
|
|
|
3660
3712
|
).all(notePath);
|
|
3661
3713
|
return rows.map((r) => r.entity);
|
|
3662
3714
|
}
|
|
3715
|
+
function getStoredNoteLinks(stateDb2, notePath) {
|
|
3716
|
+
const rows = stateDb2.db.prepare(
|
|
3717
|
+
"SELECT target FROM note_links WHERE note_path = ?"
|
|
3718
|
+
).all(notePath);
|
|
3719
|
+
return new Set(rows.map((r) => r.target));
|
|
3720
|
+
}
|
|
3721
|
+
function updateStoredNoteLinks(stateDb2, notePath, currentLinks) {
|
|
3722
|
+
const del = stateDb2.db.prepare("DELETE FROM note_links WHERE note_path = ?");
|
|
3723
|
+
const ins = stateDb2.db.prepare("INSERT INTO note_links (note_path, target) VALUES (?, ?)");
|
|
3724
|
+
const tx = stateDb2.db.transaction(() => {
|
|
3725
|
+
del.run(notePath);
|
|
3726
|
+
for (const target of currentLinks) {
|
|
3727
|
+
ins.run(notePath, target);
|
|
3728
|
+
}
|
|
3729
|
+
});
|
|
3730
|
+
tx();
|
|
3731
|
+
}
|
|
3732
|
+
function diffNoteLinks(previous, current) {
|
|
3733
|
+
return {
|
|
3734
|
+
added: [...current].filter((l) => !previous.has(l)),
|
|
3735
|
+
removed: [...previous].filter((l) => !current.has(l))
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
function getStoredNoteTags(stateDb2, notePath) {
|
|
3739
|
+
const rows = stateDb2.db.prepare(
|
|
3740
|
+
"SELECT tag FROM note_tags WHERE note_path = ?"
|
|
3741
|
+
).all(notePath);
|
|
3742
|
+
return new Set(rows.map((r) => r.tag));
|
|
3743
|
+
}
|
|
3744
|
+
function updateStoredNoteTags(stateDb2, notePath, currentTags) {
|
|
3745
|
+
const del = stateDb2.db.prepare("DELETE FROM note_tags WHERE note_path = ?");
|
|
3746
|
+
const ins = stateDb2.db.prepare("INSERT INTO note_tags (note_path, tag) VALUES (?, ?)");
|
|
3747
|
+
const tx = stateDb2.db.transaction(() => {
|
|
3748
|
+
del.run(notePath);
|
|
3749
|
+
for (const tag of currentTags) {
|
|
3750
|
+
ins.run(notePath, tag);
|
|
3751
|
+
}
|
|
3752
|
+
});
|
|
3753
|
+
tx();
|
|
3754
|
+
}
|
|
3663
3755
|
function processImplicitFeedback(stateDb2, notePath, currentContent) {
|
|
3664
3756
|
const tracked = getTrackedApplications(stateDb2, notePath);
|
|
3665
3757
|
if (tracked.length === 0) return [];
|
|
@@ -3927,7 +4019,7 @@ function getExtendedDashboardData(stateDb2) {
|
|
|
3927
4019
|
|
|
3928
4020
|
// src/core/write/git.ts
|
|
3929
4021
|
import { simpleGit, CheckRepoActions } from "simple-git";
|
|
3930
|
-
import
|
|
4022
|
+
import path8 from "path";
|
|
3931
4023
|
import fs6 from "fs/promises";
|
|
3932
4024
|
import {
|
|
3933
4025
|
setWriteState,
|
|
@@ -3981,10 +4073,10 @@ function clearLastMutationCommit() {
|
|
|
3981
4073
|
}
|
|
3982
4074
|
}
|
|
3983
4075
|
async function checkGitLock(vaultPath2) {
|
|
3984
|
-
const lockPath =
|
|
4076
|
+
const lockPath = path8.join(vaultPath2, ".git/index.lock");
|
|
3985
4077
|
try {
|
|
3986
|
-
const
|
|
3987
|
-
const ageMs = Date.now() -
|
|
4078
|
+
const stat4 = await fs6.stat(lockPath);
|
|
4079
|
+
const ageMs = Date.now() - stat4.mtimeMs;
|
|
3988
4080
|
return {
|
|
3989
4081
|
locked: true,
|
|
3990
4082
|
stale: ageMs > STALE_LOCK_THRESHOLD_MS,
|
|
@@ -4004,10 +4096,10 @@ async function isGitRepo(vaultPath2) {
|
|
|
4004
4096
|
}
|
|
4005
4097
|
}
|
|
4006
4098
|
async function checkLockFile(vaultPath2) {
|
|
4007
|
-
const lockPath =
|
|
4099
|
+
const lockPath = path8.join(vaultPath2, ".git/index.lock");
|
|
4008
4100
|
try {
|
|
4009
|
-
const
|
|
4010
|
-
const ageMs = Date.now() -
|
|
4101
|
+
const stat4 = await fs6.stat(lockPath);
|
|
4102
|
+
const ageMs = Date.now() - stat4.mtimeMs;
|
|
4011
4103
|
return { stale: ageMs > STALE_LOCK_THRESHOLD_MS, ageMs };
|
|
4012
4104
|
} catch {
|
|
4013
4105
|
return null;
|
|
@@ -4054,7 +4146,7 @@ async function commitChange(vaultPath2, filePath, messagePrefix, retryConfig = D
|
|
|
4054
4146
|
}
|
|
4055
4147
|
}
|
|
4056
4148
|
await git.add(filePath);
|
|
4057
|
-
const fileName =
|
|
4149
|
+
const fileName = path8.basename(filePath);
|
|
4058
4150
|
const commitMessage = `${messagePrefix} Update ${fileName}`;
|
|
4059
4151
|
const result = await git.commit(commitMessage);
|
|
4060
4152
|
if (result.commit) {
|
|
@@ -4248,7 +4340,7 @@ function setHintsStateDb(stateDb2) {
|
|
|
4248
4340
|
|
|
4249
4341
|
// src/core/shared/recency.ts
|
|
4250
4342
|
import { readdir, readFile, stat } from "fs/promises";
|
|
4251
|
-
import
|
|
4343
|
+
import path9 from "path";
|
|
4252
4344
|
import {
|
|
4253
4345
|
getEntityName,
|
|
4254
4346
|
recordEntityMention,
|
|
@@ -4270,9 +4362,9 @@ async function* walkMarkdownFiles(dir, baseDir) {
|
|
|
4270
4362
|
try {
|
|
4271
4363
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
4272
4364
|
for (const entry of entries) {
|
|
4273
|
-
const fullPath =
|
|
4274
|
-
const relativePath =
|
|
4275
|
-
const topFolder = relativePath.split(
|
|
4365
|
+
const fullPath = path9.join(dir, entry.name);
|
|
4366
|
+
const relativePath = path9.relative(baseDir, fullPath);
|
|
4367
|
+
const topFolder = relativePath.split(path9.sep)[0];
|
|
4276
4368
|
if (EXCLUDED_FOLDERS.has(topFolder)) {
|
|
4277
4369
|
continue;
|
|
4278
4370
|
}
|
|
@@ -5152,7 +5244,7 @@ function tokenize(text) {
|
|
|
5152
5244
|
|
|
5153
5245
|
// src/core/shared/cooccurrence.ts
|
|
5154
5246
|
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
5155
|
-
import
|
|
5247
|
+
import path10 from "path";
|
|
5156
5248
|
var DEFAULT_MIN_COOCCURRENCE = 2;
|
|
5157
5249
|
var EXCLUDED_FOLDERS2 = /* @__PURE__ */ new Set([
|
|
5158
5250
|
"templates",
|
|
@@ -5186,9 +5278,9 @@ async function* walkMarkdownFiles2(dir, baseDir) {
|
|
|
5186
5278
|
try {
|
|
5187
5279
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
5188
5280
|
for (const entry of entries) {
|
|
5189
|
-
const fullPath =
|
|
5190
|
-
const relativePath =
|
|
5191
|
-
const topFolder = relativePath.split(
|
|
5281
|
+
const fullPath = path10.join(dir, entry.name);
|
|
5282
|
+
const relativePath = path10.relative(baseDir, fullPath);
|
|
5283
|
+
const topFolder = relativePath.split(path10.sep)[0];
|
|
5192
5284
|
if (EXCLUDED_FOLDERS2.has(topFolder)) {
|
|
5193
5285
|
continue;
|
|
5194
5286
|
}
|
|
@@ -5429,7 +5521,7 @@ function sortEntitiesByPriority(entities, notePath) {
|
|
|
5429
5521
|
return priorityB - priorityA;
|
|
5430
5522
|
});
|
|
5431
5523
|
}
|
|
5432
|
-
function processWikilinks(content, notePath) {
|
|
5524
|
+
function processWikilinks(content, notePath, existingContent) {
|
|
5433
5525
|
if (!isEntityIndexReady() || !entityIndex) {
|
|
5434
5526
|
console.error("[Flywheel:DEBUG] Entity index not ready, entities:", entityIndex?._metadata?.total_entities ?? 0);
|
|
5435
5527
|
return {
|
|
@@ -5452,6 +5544,11 @@ function processWikilinks(content, notePath) {
|
|
|
5452
5544
|
caseInsensitive: true
|
|
5453
5545
|
});
|
|
5454
5546
|
const step1LinkedEntities = new Set(resolved.linkedEntities.map((e) => e.toLowerCase()));
|
|
5547
|
+
if (existingContent) {
|
|
5548
|
+
for (const match of existingContent.matchAll(/\[\[([^\]|]+?)(?:\|[^\]]+?)?\]\]/g)) {
|
|
5549
|
+
step1LinkedEntities.add(match[1].toLowerCase());
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5455
5552
|
const result = applyWikilinks(resolved.content, sortedEntities, {
|
|
5456
5553
|
firstOccurrenceOnly: true,
|
|
5457
5554
|
caseInsensitive: true,
|
|
@@ -5512,12 +5609,12 @@ function processWikilinks(content, notePath) {
|
|
|
5512
5609
|
linkedEntities: [...resolved.linkedEntities, ...result.linkedEntities]
|
|
5513
5610
|
};
|
|
5514
5611
|
}
|
|
5515
|
-
function maybeApplyWikilinks(content, skipWikilinks, notePath) {
|
|
5612
|
+
function maybeApplyWikilinks(content, skipWikilinks, notePath, existingContent) {
|
|
5516
5613
|
if (skipWikilinks) {
|
|
5517
5614
|
return { content };
|
|
5518
5615
|
}
|
|
5519
5616
|
checkAndRefreshIfStale();
|
|
5520
|
-
const result = processWikilinks(content, notePath);
|
|
5617
|
+
const result = processWikilinks(content, notePath, existingContent);
|
|
5521
5618
|
if (result.linksAdded > 0) {
|
|
5522
5619
|
if (moduleStateDb4 && notePath) {
|
|
5523
5620
|
trackWikilinkApplications(moduleStateDb4, notePath, result.linkedEntities);
|
|
@@ -6502,11 +6599,11 @@ function countFTS5Mentions(term) {
|
|
|
6502
6599
|
}
|
|
6503
6600
|
|
|
6504
6601
|
// src/core/read/taskCache.ts
|
|
6505
|
-
import * as
|
|
6602
|
+
import * as path12 from "path";
|
|
6506
6603
|
|
|
6507
6604
|
// src/tools/read/tasks.ts
|
|
6508
6605
|
import * as fs8 from "fs";
|
|
6509
|
-
import * as
|
|
6606
|
+
import * as path11 from "path";
|
|
6510
6607
|
var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
|
|
6511
6608
|
var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
|
|
6512
6609
|
var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
|
|
@@ -6575,7 +6672,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
|
|
|
6575
6672
|
const allTasks = [];
|
|
6576
6673
|
for (const note of index.notes.values()) {
|
|
6577
6674
|
if (folder && !note.path.startsWith(folder)) continue;
|
|
6578
|
-
const absolutePath =
|
|
6675
|
+
const absolutePath = path11.join(vaultPath2, note.path);
|
|
6579
6676
|
const tasks = await extractTasksFromNote(note.path, absolutePath);
|
|
6580
6677
|
allTasks.push(...tasks);
|
|
6581
6678
|
}
|
|
@@ -6619,7 +6716,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
|
|
|
6619
6716
|
async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
|
|
6620
6717
|
const note = index.notes.get(notePath);
|
|
6621
6718
|
if (!note) return null;
|
|
6622
|
-
const absolutePath =
|
|
6719
|
+
const absolutePath = path11.join(vaultPath2, notePath);
|
|
6623
6720
|
let tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6624
6721
|
if (excludeTags.length > 0) {
|
|
6625
6722
|
tasks = tasks.filter(
|
|
@@ -6711,7 +6808,7 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
6711
6808
|
}
|
|
6712
6809
|
const allRows = [];
|
|
6713
6810
|
for (const notePath of notePaths) {
|
|
6714
|
-
const absolutePath =
|
|
6811
|
+
const absolutePath = path12.join(vaultPath2, notePath);
|
|
6715
6812
|
const tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6716
6813
|
for (const task of tasks) {
|
|
6717
6814
|
if (excludeTags?.length && excludeTags.some((t) => task.tags.includes(t))) {
|
|
@@ -6753,7 +6850,7 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
6753
6850
|
async function updateTaskCacheForFile(vaultPath2, relativePath) {
|
|
6754
6851
|
if (!db3) return;
|
|
6755
6852
|
db3.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
|
|
6756
|
-
const absolutePath =
|
|
6853
|
+
const absolutePath = path12.join(vaultPath2, relativePath);
|
|
6757
6854
|
const tasks = await extractTasksFromNote(relativePath, absolutePath);
|
|
6758
6855
|
if (tasks.length > 0) {
|
|
6759
6856
|
const insertStmt = db3.prepare(`
|
|
@@ -6893,7 +6990,7 @@ import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, get
|
|
|
6893
6990
|
|
|
6894
6991
|
// src/tools/read/graph.ts
|
|
6895
6992
|
import * as fs9 from "fs";
|
|
6896
|
-
import * as
|
|
6993
|
+
import * as path13 from "path";
|
|
6897
6994
|
import { z } from "zod";
|
|
6898
6995
|
|
|
6899
6996
|
// src/core/read/constants.ts
|
|
@@ -7177,7 +7274,7 @@ function requireIndex() {
|
|
|
7177
7274
|
// src/tools/read/graph.ts
|
|
7178
7275
|
async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
|
|
7179
7276
|
try {
|
|
7180
|
-
const fullPath =
|
|
7277
|
+
const fullPath = path13.join(vaultPath2, sourcePath);
|
|
7181
7278
|
const content = await fs9.promises.readFile(fullPath, "utf-8");
|
|
7182
7279
|
const allLines = content.split("\n");
|
|
7183
7280
|
let fmLines = 0;
|
|
@@ -7491,14 +7588,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
7491
7588
|
};
|
|
7492
7589
|
function findSimilarEntity2(target, entities) {
|
|
7493
7590
|
const targetLower = target.toLowerCase();
|
|
7494
|
-
for (const [name,
|
|
7591
|
+
for (const [name, path32] of entities) {
|
|
7495
7592
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
7496
|
-
return
|
|
7593
|
+
return path32;
|
|
7497
7594
|
}
|
|
7498
7595
|
}
|
|
7499
|
-
for (const [name,
|
|
7596
|
+
for (const [name, path32] of entities) {
|
|
7500
7597
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
7501
|
-
return
|
|
7598
|
+
return path32;
|
|
7502
7599
|
}
|
|
7503
7600
|
}
|
|
7504
7601
|
return void 0;
|
|
@@ -8475,8 +8572,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8475
8572
|
daily_counts: z3.record(z3.number())
|
|
8476
8573
|
}).describe("Activity summary for the last 7 days")
|
|
8477
8574
|
};
|
|
8478
|
-
function isPeriodicNote2(
|
|
8479
|
-
const filename =
|
|
8575
|
+
function isPeriodicNote2(path32) {
|
|
8576
|
+
const filename = path32.split("/").pop() || "";
|
|
8480
8577
|
const nameWithoutExt = filename.replace(/\.md$/, "");
|
|
8481
8578
|
const patterns = [
|
|
8482
8579
|
/^\d{4}-\d{2}-\d{2}$/,
|
|
@@ -8491,7 +8588,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8491
8588
|
// YYYY (yearly)
|
|
8492
8589
|
];
|
|
8493
8590
|
const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
|
|
8494
|
-
const folder =
|
|
8591
|
+
const folder = path32.split("/")[0]?.toLowerCase() || "";
|
|
8495
8592
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
8496
8593
|
}
|
|
8497
8594
|
server2.registerTool(
|
|
@@ -8899,7 +8996,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
8899
8996
|
|
|
8900
8997
|
// src/tools/read/system.ts
|
|
8901
8998
|
import * as fs11 from "fs";
|
|
8902
|
-
import * as
|
|
8999
|
+
import * as path14 from "path";
|
|
8903
9000
|
import { z as z5 } from "zod";
|
|
8904
9001
|
import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
|
|
8905
9002
|
|
|
@@ -9199,7 +9296,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
9199
9296
|
continue;
|
|
9200
9297
|
}
|
|
9201
9298
|
try {
|
|
9202
|
-
const fullPath =
|
|
9299
|
+
const fullPath = path14.join(vaultPath2, note.path);
|
|
9203
9300
|
const content = await fs11.promises.readFile(fullPath, "utf-8");
|
|
9204
9301
|
const lines = content.split("\n");
|
|
9205
9302
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9315,7 +9412,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
9315
9412
|
let wordCount;
|
|
9316
9413
|
if (include_word_count) {
|
|
9317
9414
|
try {
|
|
9318
|
-
const fullPath =
|
|
9415
|
+
const fullPath = path14.join(vaultPath2, resolvedPath);
|
|
9319
9416
|
const content = await fs11.promises.readFile(fullPath, "utf-8");
|
|
9320
9417
|
wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
9321
9418
|
} catch {
|
|
@@ -9546,7 +9643,7 @@ import { z as z6 } from "zod";
|
|
|
9546
9643
|
|
|
9547
9644
|
// src/tools/read/structure.ts
|
|
9548
9645
|
import * as fs12 from "fs";
|
|
9549
|
-
import * as
|
|
9646
|
+
import * as path15 from "path";
|
|
9550
9647
|
var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
|
|
9551
9648
|
function extractHeadings(content) {
|
|
9552
9649
|
const lines = content.split("\n");
|
|
@@ -9600,7 +9697,7 @@ function buildSections(headings, totalLines) {
|
|
|
9600
9697
|
async function getNoteStructure(index, notePath, vaultPath2) {
|
|
9601
9698
|
const note = index.notes.get(notePath);
|
|
9602
9699
|
if (!note) return null;
|
|
9603
|
-
const absolutePath =
|
|
9700
|
+
const absolutePath = path15.join(vaultPath2, notePath);
|
|
9604
9701
|
let content;
|
|
9605
9702
|
try {
|
|
9606
9703
|
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
@@ -9623,7 +9720,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
|
|
|
9623
9720
|
async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
|
|
9624
9721
|
const note = index.notes.get(notePath);
|
|
9625
9722
|
if (!note) return null;
|
|
9626
|
-
const absolutePath =
|
|
9723
|
+
const absolutePath = path15.join(vaultPath2, notePath);
|
|
9627
9724
|
let content;
|
|
9628
9725
|
try {
|
|
9629
9726
|
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
@@ -9665,7 +9762,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
9665
9762
|
const results = [];
|
|
9666
9763
|
for (const note of index.notes.values()) {
|
|
9667
9764
|
if (folder && !note.path.startsWith(folder)) continue;
|
|
9668
|
-
const absolutePath =
|
|
9765
|
+
const absolutePath = path15.join(vaultPath2, note.path);
|
|
9669
9766
|
let content;
|
|
9670
9767
|
try {
|
|
9671
9768
|
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
@@ -9699,18 +9796,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9699
9796
|
include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
|
|
9700
9797
|
}
|
|
9701
9798
|
},
|
|
9702
|
-
async ({ path:
|
|
9799
|
+
async ({ path: path32, include_content }) => {
|
|
9703
9800
|
const index = getIndex();
|
|
9704
9801
|
const vaultPath2 = getVaultPath();
|
|
9705
|
-
const result = await getNoteStructure(index,
|
|
9802
|
+
const result = await getNoteStructure(index, path32, vaultPath2);
|
|
9706
9803
|
if (!result) {
|
|
9707
9804
|
return {
|
|
9708
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
9805
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path32 }, null, 2) }]
|
|
9709
9806
|
};
|
|
9710
9807
|
}
|
|
9711
9808
|
if (include_content) {
|
|
9712
9809
|
for (const section of result.sections) {
|
|
9713
|
-
const sectionResult = await getSectionContent(index,
|
|
9810
|
+
const sectionResult = await getSectionContent(index, path32, section.heading.text, vaultPath2, true);
|
|
9714
9811
|
if (sectionResult) {
|
|
9715
9812
|
section.content = sectionResult.content;
|
|
9716
9813
|
}
|
|
@@ -9732,15 +9829,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9732
9829
|
include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
|
|
9733
9830
|
}
|
|
9734
9831
|
},
|
|
9735
|
-
async ({ path:
|
|
9832
|
+
async ({ path: path32, heading, include_subheadings }) => {
|
|
9736
9833
|
const index = getIndex();
|
|
9737
9834
|
const vaultPath2 = getVaultPath();
|
|
9738
|
-
const result = await getSectionContent(index,
|
|
9835
|
+
const result = await getSectionContent(index, path32, heading, vaultPath2, include_subheadings);
|
|
9739
9836
|
if (!result) {
|
|
9740
9837
|
return {
|
|
9741
9838
|
content: [{ type: "text", text: JSON.stringify({
|
|
9742
9839
|
error: "Section not found",
|
|
9743
|
-
path:
|
|
9840
|
+
path: path32,
|
|
9744
9841
|
heading
|
|
9745
9842
|
}, null, 2) }]
|
|
9746
9843
|
};
|
|
@@ -9794,16 +9891,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9794
9891
|
offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
9795
9892
|
}
|
|
9796
9893
|
},
|
|
9797
|
-
async ({ path:
|
|
9894
|
+
async ({ path: path32, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
|
|
9798
9895
|
const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
|
|
9799
9896
|
const index = getIndex();
|
|
9800
9897
|
const vaultPath2 = getVaultPath();
|
|
9801
9898
|
const config = getConfig();
|
|
9802
|
-
if (
|
|
9803
|
-
const result2 = await getTasksFromNote(index,
|
|
9899
|
+
if (path32) {
|
|
9900
|
+
const result2 = await getTasksFromNote(index, path32, vaultPath2, config.exclude_task_tags || []);
|
|
9804
9901
|
if (!result2) {
|
|
9805
9902
|
return {
|
|
9806
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
9903
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path32 }, null, 2) }]
|
|
9807
9904
|
};
|
|
9808
9905
|
}
|
|
9809
9906
|
let filtered = result2;
|
|
@@ -9813,7 +9910,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9813
9910
|
const paged2 = filtered.slice(offset, offset + limit);
|
|
9814
9911
|
return {
|
|
9815
9912
|
content: [{ type: "text", text: JSON.stringify({
|
|
9816
|
-
path:
|
|
9913
|
+
path: path32,
|
|
9817
9914
|
total_count: filtered.length,
|
|
9818
9915
|
returned_count: paged2.length,
|
|
9819
9916
|
open: result2.filter((t) => t.status === "open").length,
|
|
@@ -9969,7 +10066,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9969
10066
|
// src/tools/read/migrations.ts
|
|
9970
10067
|
import { z as z7 } from "zod";
|
|
9971
10068
|
import * as fs13 from "fs/promises";
|
|
9972
|
-
import * as
|
|
10069
|
+
import * as path16 from "path";
|
|
9973
10070
|
import matter2 from "gray-matter";
|
|
9974
10071
|
function getNotesInFolder(index, folder) {
|
|
9975
10072
|
const notes = [];
|
|
@@ -9982,7 +10079,7 @@ function getNotesInFolder(index, folder) {
|
|
|
9982
10079
|
return notes;
|
|
9983
10080
|
}
|
|
9984
10081
|
async function readFileContent(notePath, vaultPath2) {
|
|
9985
|
-
const fullPath =
|
|
10082
|
+
const fullPath = path16.join(vaultPath2, notePath);
|
|
9986
10083
|
try {
|
|
9987
10084
|
return await fs13.readFile(fullPath, "utf-8");
|
|
9988
10085
|
} catch {
|
|
@@ -9990,7 +10087,7 @@ async function readFileContent(notePath, vaultPath2) {
|
|
|
9990
10087
|
}
|
|
9991
10088
|
}
|
|
9992
10089
|
async function writeFileContent(notePath, vaultPath2, content) {
|
|
9993
|
-
const fullPath =
|
|
10090
|
+
const fullPath = path16.join(vaultPath2, notePath);
|
|
9994
10091
|
try {
|
|
9995
10092
|
await fs13.writeFile(fullPath, content, "utf-8");
|
|
9996
10093
|
return true;
|
|
@@ -10171,7 +10268,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
|
|
|
10171
10268
|
|
|
10172
10269
|
// src/tools/read/graphAnalysis.ts
|
|
10173
10270
|
import fs14 from "node:fs";
|
|
10174
|
-
import
|
|
10271
|
+
import path17 from "node:path";
|
|
10175
10272
|
import { z as z8 } from "zod";
|
|
10176
10273
|
|
|
10177
10274
|
// src/tools/read/schema.ts
|
|
@@ -10933,7 +11030,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
10933
11030
|
const scored = allNotes.map((note) => {
|
|
10934
11031
|
let wordCount = 0;
|
|
10935
11032
|
try {
|
|
10936
|
-
const content = fs14.readFileSync(
|
|
11033
|
+
const content = fs14.readFileSync(path17.join(vaultPath2, note.path), "utf-8");
|
|
10937
11034
|
const body = content.replace(/^---[\s\S]*?---\n?/, "");
|
|
10938
11035
|
wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
|
|
10939
11036
|
} catch {
|
|
@@ -11513,12 +11610,12 @@ import { z as z10 } from "zod";
|
|
|
11513
11610
|
|
|
11514
11611
|
// src/tools/read/bidirectional.ts
|
|
11515
11612
|
import * as fs15 from "fs/promises";
|
|
11516
|
-
import * as
|
|
11613
|
+
import * as path18 from "path";
|
|
11517
11614
|
import matter3 from "gray-matter";
|
|
11518
11615
|
var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
|
|
11519
11616
|
var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
|
|
11520
11617
|
async function readFileContent2(notePath, vaultPath2) {
|
|
11521
|
-
const fullPath =
|
|
11618
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
11522
11619
|
try {
|
|
11523
11620
|
return await fs15.readFile(fullPath, "utf-8");
|
|
11524
11621
|
} catch {
|
|
@@ -11697,10 +11794,10 @@ async function suggestWikilinksInFrontmatter(index, notePath, vaultPath2) {
|
|
|
11697
11794
|
|
|
11698
11795
|
// src/tools/read/computed.ts
|
|
11699
11796
|
import * as fs16 from "fs/promises";
|
|
11700
|
-
import * as
|
|
11797
|
+
import * as path19 from "path";
|
|
11701
11798
|
import matter4 from "gray-matter";
|
|
11702
11799
|
async function readFileContent3(notePath, vaultPath2) {
|
|
11703
|
-
const fullPath =
|
|
11800
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
11704
11801
|
try {
|
|
11705
11802
|
return await fs16.readFile(fullPath, "utf-8");
|
|
11706
11803
|
} catch {
|
|
@@ -11708,7 +11805,7 @@ async function readFileContent3(notePath, vaultPath2) {
|
|
|
11708
11805
|
}
|
|
11709
11806
|
}
|
|
11710
11807
|
async function getFileStats(notePath, vaultPath2) {
|
|
11711
|
-
const fullPath =
|
|
11808
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
11712
11809
|
try {
|
|
11713
11810
|
const stats = await fs16.stat(fullPath);
|
|
11714
11811
|
return {
|
|
@@ -11979,7 +12076,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath, getConfi
|
|
|
11979
12076
|
init_writer();
|
|
11980
12077
|
import { z as z11 } from "zod";
|
|
11981
12078
|
import fs20 from "fs/promises";
|
|
11982
|
-
import
|
|
12079
|
+
import path22 from "path";
|
|
11983
12080
|
|
|
11984
12081
|
// src/core/write/validator.ts
|
|
11985
12082
|
var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
|
|
@@ -12182,7 +12279,7 @@ function runValidationPipeline(content, format, options = {}) {
|
|
|
12182
12279
|
// src/core/write/mutation-helpers.ts
|
|
12183
12280
|
init_writer();
|
|
12184
12281
|
import fs19 from "fs/promises";
|
|
12185
|
-
import
|
|
12282
|
+
import path21 from "path";
|
|
12186
12283
|
init_constants();
|
|
12187
12284
|
init_writer();
|
|
12188
12285
|
function formatMcpResult(result) {
|
|
@@ -12231,7 +12328,7 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
|
|
|
12231
12328
|
return info;
|
|
12232
12329
|
}
|
|
12233
12330
|
async function ensureFileExists(vaultPath2, notePath) {
|
|
12234
|
-
const fullPath =
|
|
12331
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
12235
12332
|
try {
|
|
12236
12333
|
await fs19.access(fullPath);
|
|
12237
12334
|
return null;
|
|
@@ -12290,7 +12387,7 @@ async function withVaultFile(options, operation) {
|
|
|
12290
12387
|
if ("error" in result) {
|
|
12291
12388
|
return formatMcpResult(result.error);
|
|
12292
12389
|
}
|
|
12293
|
-
const fullPath =
|
|
12390
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
12294
12391
|
const statBefore = await fs19.stat(fullPath);
|
|
12295
12392
|
if (statBefore.mtimeMs !== result.mtimeMs) {
|
|
12296
12393
|
console.warn(`[withVaultFile] External modification detected on ${notePath}, re-reading and retrying`);
|
|
@@ -12353,10 +12450,10 @@ async function withVaultFrontmatter(options, operation) {
|
|
|
12353
12450
|
|
|
12354
12451
|
// src/tools/write/mutations.ts
|
|
12355
12452
|
async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
12356
|
-
const fullPath =
|
|
12357
|
-
await fs20.mkdir(
|
|
12453
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
12454
|
+
await fs20.mkdir(path22.dirname(fullPath), { recursive: true });
|
|
12358
12455
|
const templates = config.templates || {};
|
|
12359
|
-
const filename =
|
|
12456
|
+
const filename = path22.basename(notePath, ".md").toLowerCase();
|
|
12360
12457
|
let templatePath;
|
|
12361
12458
|
const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
|
|
12362
12459
|
const weeklyPattern = /^\d{4}-W\d{2}/;
|
|
@@ -12377,10 +12474,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
12377
12474
|
let templateContent;
|
|
12378
12475
|
if (templatePath) {
|
|
12379
12476
|
try {
|
|
12380
|
-
const absTemplatePath =
|
|
12477
|
+
const absTemplatePath = path22.join(vaultPath2, templatePath);
|
|
12381
12478
|
templateContent = await fs20.readFile(absTemplatePath, "utf-8");
|
|
12382
12479
|
} catch {
|
|
12383
|
-
const title =
|
|
12480
|
+
const title = path22.basename(notePath, ".md");
|
|
12384
12481
|
templateContent = `---
|
|
12385
12482
|
---
|
|
12386
12483
|
|
|
@@ -12389,7 +12486,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
12389
12486
|
templatePath = void 0;
|
|
12390
12487
|
}
|
|
12391
12488
|
} else {
|
|
12392
|
-
const title =
|
|
12489
|
+
const title = path22.basename(notePath, ".md");
|
|
12393
12490
|
templateContent = `---
|
|
12394
12491
|
---
|
|
12395
12492
|
|
|
@@ -12398,7 +12495,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
12398
12495
|
}
|
|
12399
12496
|
const now = /* @__PURE__ */ new Date();
|
|
12400
12497
|
const dateStr = now.toISOString().split("T")[0];
|
|
12401
|
-
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g,
|
|
12498
|
+
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path22.basename(notePath, ".md"));
|
|
12402
12499
|
const matter9 = (await import("gray-matter")).default;
|
|
12403
12500
|
const parsed = matter9(templateContent);
|
|
12404
12501
|
if (!parsed.data.date) {
|
|
@@ -12438,7 +12535,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
12438
12535
|
let noteCreated = false;
|
|
12439
12536
|
let templateUsed;
|
|
12440
12537
|
if (create_if_missing) {
|
|
12441
|
-
const fullPath =
|
|
12538
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
12442
12539
|
try {
|
|
12443
12540
|
await fs20.access(fullPath);
|
|
12444
12541
|
} catch {
|
|
@@ -12468,7 +12565,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
12468
12565
|
throw new Error(validationResult.blockReason || "Output validation failed");
|
|
12469
12566
|
}
|
|
12470
12567
|
let workingContent = validationResult.content;
|
|
12471
|
-
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(workingContent, skipWikilinks, notePath);
|
|
12568
|
+
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(workingContent, skipWikilinks, notePath, ctx.content);
|
|
12472
12569
|
if (linkedEntities?.length) {
|
|
12473
12570
|
const stateDb2 = getWriteStateDb();
|
|
12474
12571
|
if (stateDb2) {
|
|
@@ -12897,7 +12994,7 @@ Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { st
|
|
|
12897
12994
|
init_writer();
|
|
12898
12995
|
import { z as z14 } from "zod";
|
|
12899
12996
|
import fs21 from "fs/promises";
|
|
12900
|
-
import
|
|
12997
|
+
import path23 from "path";
|
|
12901
12998
|
function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
12902
12999
|
server2.tool(
|
|
12903
13000
|
"vault_create_note",
|
|
@@ -12920,23 +13017,23 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
12920
13017
|
if (!validatePath(vaultPath2, notePath)) {
|
|
12921
13018
|
return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
|
|
12922
13019
|
}
|
|
12923
|
-
const fullPath =
|
|
13020
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
12924
13021
|
const existsCheck = await ensureFileExists(vaultPath2, notePath);
|
|
12925
13022
|
if (existsCheck === null && !overwrite) {
|
|
12926
13023
|
return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
|
|
12927
13024
|
}
|
|
12928
|
-
const dir =
|
|
13025
|
+
const dir = path23.dirname(fullPath);
|
|
12929
13026
|
await fs21.mkdir(dir, { recursive: true });
|
|
12930
13027
|
let effectiveContent = content;
|
|
12931
13028
|
let effectiveFrontmatter = frontmatter;
|
|
12932
13029
|
if (template) {
|
|
12933
|
-
const templatePath =
|
|
13030
|
+
const templatePath = path23.join(vaultPath2, template);
|
|
12934
13031
|
try {
|
|
12935
13032
|
const raw = await fs21.readFile(templatePath, "utf-8");
|
|
12936
13033
|
const matter9 = (await import("gray-matter")).default;
|
|
12937
13034
|
const parsed = matter9(raw);
|
|
12938
13035
|
const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
12939
|
-
const title =
|
|
13036
|
+
const title = path23.basename(notePath, ".md");
|
|
12940
13037
|
let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
|
|
12941
13038
|
if (content) {
|
|
12942
13039
|
templateContent = templateContent.trimEnd() + "\n\n" + content;
|
|
@@ -12955,7 +13052,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
12955
13052
|
effectiveFrontmatter.created = now.toISOString();
|
|
12956
13053
|
}
|
|
12957
13054
|
const warnings = [];
|
|
12958
|
-
const noteName =
|
|
13055
|
+
const noteName = path23.basename(notePath, ".md");
|
|
12959
13056
|
const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
|
|
12960
13057
|
const preflight = await checkPreflightSimilarity(noteName);
|
|
12961
13058
|
if (preflight.existingEntity) {
|
|
@@ -13072,7 +13169,7 @@ ${sources}`;
|
|
|
13072
13169
|
}
|
|
13073
13170
|
return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
|
|
13074
13171
|
}
|
|
13075
|
-
const fullPath =
|
|
13172
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
13076
13173
|
await fs21.unlink(fullPath);
|
|
13077
13174
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
|
|
13078
13175
|
const message = backlinkWarning ? `Deleted note: ${notePath}
|
|
@@ -13092,7 +13189,7 @@ Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
|
|
|
13092
13189
|
init_writer();
|
|
13093
13190
|
import { z as z15 } from "zod";
|
|
13094
13191
|
import fs22 from "fs/promises";
|
|
13095
|
-
import
|
|
13192
|
+
import path24 from "path";
|
|
13096
13193
|
import matter6 from "gray-matter";
|
|
13097
13194
|
function escapeRegex(str) {
|
|
13098
13195
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -13111,7 +13208,7 @@ function extractWikilinks2(content) {
|
|
|
13111
13208
|
return wikilinks;
|
|
13112
13209
|
}
|
|
13113
13210
|
function getTitleFromPath(filePath) {
|
|
13114
|
-
return
|
|
13211
|
+
return path24.basename(filePath, ".md");
|
|
13115
13212
|
}
|
|
13116
13213
|
async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
13117
13214
|
const results = [];
|
|
@@ -13120,7 +13217,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
13120
13217
|
const files = [];
|
|
13121
13218
|
const entries = await fs22.readdir(dir, { withFileTypes: true });
|
|
13122
13219
|
for (const entry of entries) {
|
|
13123
|
-
const fullPath =
|
|
13220
|
+
const fullPath = path24.join(dir, entry.name);
|
|
13124
13221
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
13125
13222
|
files.push(...await scanDir(fullPath));
|
|
13126
13223
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -13131,7 +13228,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
13131
13228
|
}
|
|
13132
13229
|
const allFiles = await scanDir(vaultPath2);
|
|
13133
13230
|
for (const filePath of allFiles) {
|
|
13134
|
-
const relativePath =
|
|
13231
|
+
const relativePath = path24.relative(vaultPath2, filePath);
|
|
13135
13232
|
const content = await fs22.readFile(filePath, "utf-8");
|
|
13136
13233
|
const wikilinks = extractWikilinks2(content);
|
|
13137
13234
|
const matchingLinks = [];
|
|
@@ -13151,7 +13248,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
13151
13248
|
return results;
|
|
13152
13249
|
}
|
|
13153
13250
|
async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
|
|
13154
|
-
const fullPath =
|
|
13251
|
+
const fullPath = path24.join(vaultPath2, filePath);
|
|
13155
13252
|
const raw = await fs22.readFile(fullPath, "utf-8");
|
|
13156
13253
|
const parsed = matter6(raw);
|
|
13157
13254
|
let content = parsed.content;
|
|
@@ -13218,8 +13315,8 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
13218
13315
|
};
|
|
13219
13316
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
13220
13317
|
}
|
|
13221
|
-
const oldFullPath =
|
|
13222
|
-
const newFullPath =
|
|
13318
|
+
const oldFullPath = path24.join(vaultPath2, oldPath);
|
|
13319
|
+
const newFullPath = path24.join(vaultPath2, newPath);
|
|
13223
13320
|
try {
|
|
13224
13321
|
await fs22.access(oldFullPath);
|
|
13225
13322
|
} catch {
|
|
@@ -13269,7 +13366,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
13269
13366
|
}
|
|
13270
13367
|
}
|
|
13271
13368
|
}
|
|
13272
|
-
const destDir =
|
|
13369
|
+
const destDir = path24.dirname(newFullPath);
|
|
13273
13370
|
await fs22.mkdir(destDir, { recursive: true });
|
|
13274
13371
|
await fs22.rename(oldFullPath, newFullPath);
|
|
13275
13372
|
let gitCommit;
|
|
@@ -13355,10 +13452,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
13355
13452
|
if (sanitizedTitle !== newTitle) {
|
|
13356
13453
|
console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
|
|
13357
13454
|
}
|
|
13358
|
-
const fullPath =
|
|
13359
|
-
const dir =
|
|
13360
|
-
const newPath = dir === "." ? `${sanitizedTitle}.md` :
|
|
13361
|
-
const newFullPath =
|
|
13455
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
13456
|
+
const dir = path24.dirname(notePath);
|
|
13457
|
+
const newPath = dir === "." ? `${sanitizedTitle}.md` : path24.join(dir, `${sanitizedTitle}.md`);
|
|
13458
|
+
const newFullPath = path24.join(vaultPath2, newPath);
|
|
13362
13459
|
try {
|
|
13363
13460
|
await fs22.access(fullPath);
|
|
13364
13461
|
} catch {
|
|
@@ -13715,7 +13812,7 @@ init_schema();
|
|
|
13715
13812
|
// src/core/write/policy/parser.ts
|
|
13716
13813
|
init_schema();
|
|
13717
13814
|
import fs24 from "fs/promises";
|
|
13718
|
-
import
|
|
13815
|
+
import path25 from "path";
|
|
13719
13816
|
import matter7 from "gray-matter";
|
|
13720
13817
|
function parseYaml(content) {
|
|
13721
13818
|
const parsed = matter7(`---
|
|
@@ -13764,13 +13861,13 @@ async function loadPolicyFile(filePath) {
|
|
|
13764
13861
|
}
|
|
13765
13862
|
}
|
|
13766
13863
|
async function loadPolicy(vaultPath2, policyName) {
|
|
13767
|
-
const policiesDir =
|
|
13768
|
-
const policyPath =
|
|
13864
|
+
const policiesDir = path25.join(vaultPath2, ".claude", "policies");
|
|
13865
|
+
const policyPath = path25.join(policiesDir, `${policyName}.yaml`);
|
|
13769
13866
|
try {
|
|
13770
13867
|
await fs24.access(policyPath);
|
|
13771
13868
|
return loadPolicyFile(policyPath);
|
|
13772
13869
|
} catch {
|
|
13773
|
-
const ymlPath =
|
|
13870
|
+
const ymlPath = path25.join(policiesDir, `${policyName}.yml`);
|
|
13774
13871
|
try {
|
|
13775
13872
|
await fs24.access(ymlPath);
|
|
13776
13873
|
return loadPolicyFile(ymlPath);
|
|
@@ -13911,7 +14008,7 @@ init_conditions();
|
|
|
13911
14008
|
init_schema();
|
|
13912
14009
|
init_writer();
|
|
13913
14010
|
import fs26 from "fs/promises";
|
|
13914
|
-
import
|
|
14011
|
+
import path27 from "path";
|
|
13915
14012
|
init_constants();
|
|
13916
14013
|
async function executeStep(step, vaultPath2, context, conditionResults) {
|
|
13917
14014
|
const { execute, reason } = shouldStepExecute(step.when, conditionResults);
|
|
@@ -13985,7 +14082,7 @@ async function executeAddToSection(params, vaultPath2, context) {
|
|
|
13985
14082
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
13986
14083
|
const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
|
|
13987
14084
|
const maxSuggestions = Number(params.maxSuggestions) || 3;
|
|
13988
|
-
const fullPath =
|
|
14085
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
13989
14086
|
try {
|
|
13990
14087
|
await fs26.access(fullPath);
|
|
13991
14088
|
} catch {
|
|
@@ -14025,7 +14122,7 @@ async function executeRemoveFromSection(params, vaultPath2) {
|
|
|
14025
14122
|
const pattern = String(params.pattern || "");
|
|
14026
14123
|
const mode = params.mode || "first";
|
|
14027
14124
|
const useRegex = Boolean(params.useRegex);
|
|
14028
|
-
const fullPath =
|
|
14125
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14029
14126
|
try {
|
|
14030
14127
|
await fs26.access(fullPath);
|
|
14031
14128
|
} catch {
|
|
@@ -14056,7 +14153,7 @@ async function executeReplaceInSection(params, vaultPath2, context) {
|
|
|
14056
14153
|
const mode = params.mode || "first";
|
|
14057
14154
|
const useRegex = Boolean(params.useRegex);
|
|
14058
14155
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
14059
|
-
const fullPath =
|
|
14156
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14060
14157
|
try {
|
|
14061
14158
|
await fs26.access(fullPath);
|
|
14062
14159
|
} catch {
|
|
@@ -14099,7 +14196,7 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
14099
14196
|
if (!validatePath(vaultPath2, notePath)) {
|
|
14100
14197
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
14101
14198
|
}
|
|
14102
|
-
const fullPath =
|
|
14199
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14103
14200
|
try {
|
|
14104
14201
|
await fs26.access(fullPath);
|
|
14105
14202
|
if (!overwrite) {
|
|
@@ -14107,7 +14204,7 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
14107
14204
|
}
|
|
14108
14205
|
} catch {
|
|
14109
14206
|
}
|
|
14110
|
-
const dir =
|
|
14207
|
+
const dir = path27.dirname(fullPath);
|
|
14111
14208
|
await fs26.mkdir(dir, { recursive: true });
|
|
14112
14209
|
const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
|
|
14113
14210
|
await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
|
|
@@ -14127,7 +14224,7 @@ async function executeDeleteNote(params, vaultPath2) {
|
|
|
14127
14224
|
if (!validatePath(vaultPath2, notePath)) {
|
|
14128
14225
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
14129
14226
|
}
|
|
14130
|
-
const fullPath =
|
|
14227
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14131
14228
|
try {
|
|
14132
14229
|
await fs26.access(fullPath);
|
|
14133
14230
|
} catch {
|
|
@@ -14144,7 +14241,7 @@ async function executeToggleTask(params, vaultPath2) {
|
|
|
14144
14241
|
const notePath = String(params.path || "");
|
|
14145
14242
|
const task = String(params.task || "");
|
|
14146
14243
|
const section = params.section ? String(params.section) : void 0;
|
|
14147
|
-
const fullPath =
|
|
14244
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14148
14245
|
try {
|
|
14149
14246
|
await fs26.access(fullPath);
|
|
14150
14247
|
} catch {
|
|
@@ -14187,7 +14284,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
14187
14284
|
const completed = Boolean(params.completed);
|
|
14188
14285
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
14189
14286
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
14190
|
-
const fullPath =
|
|
14287
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14191
14288
|
try {
|
|
14192
14289
|
await fs26.access(fullPath);
|
|
14193
14290
|
} catch {
|
|
@@ -14224,7 +14321,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
14224
14321
|
async function executeUpdateFrontmatter(params, vaultPath2) {
|
|
14225
14322
|
const notePath = String(params.path || "");
|
|
14226
14323
|
const updates = params.frontmatter || {};
|
|
14227
|
-
const fullPath =
|
|
14324
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14228
14325
|
try {
|
|
14229
14326
|
await fs26.access(fullPath);
|
|
14230
14327
|
} catch {
|
|
@@ -14246,7 +14343,7 @@ async function executeAddFrontmatterField(params, vaultPath2) {
|
|
|
14246
14343
|
const notePath = String(params.path || "");
|
|
14247
14344
|
const key = String(params.key || "");
|
|
14248
14345
|
const value = params.value;
|
|
14249
|
-
const fullPath =
|
|
14346
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14250
14347
|
try {
|
|
14251
14348
|
await fs26.access(fullPath);
|
|
14252
14349
|
} catch {
|
|
@@ -14409,7 +14506,7 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
|
|
|
14409
14506
|
async function rollbackChanges(vaultPath2, originalContents, filesModified) {
|
|
14410
14507
|
for (const filePath of filesModified) {
|
|
14411
14508
|
const original = originalContents.get(filePath);
|
|
14412
|
-
const fullPath =
|
|
14509
|
+
const fullPath = path27.join(vaultPath2, filePath);
|
|
14413
14510
|
if (original === null) {
|
|
14414
14511
|
try {
|
|
14415
14512
|
await fs26.unlink(fullPath);
|
|
@@ -14464,9 +14561,9 @@ async function previewPolicy(policy, vaultPath2, variables) {
|
|
|
14464
14561
|
|
|
14465
14562
|
// src/core/write/policy/storage.ts
|
|
14466
14563
|
import fs27 from "fs/promises";
|
|
14467
|
-
import
|
|
14564
|
+
import path28 from "path";
|
|
14468
14565
|
function getPoliciesDir(vaultPath2) {
|
|
14469
|
-
return
|
|
14566
|
+
return path28.join(vaultPath2, ".claude", "policies");
|
|
14470
14567
|
}
|
|
14471
14568
|
async function ensurePoliciesDir(vaultPath2) {
|
|
14472
14569
|
const dir = getPoliciesDir(vaultPath2);
|
|
@@ -14481,15 +14578,15 @@ async function listPolicies(vaultPath2) {
|
|
|
14481
14578
|
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
|
|
14482
14579
|
continue;
|
|
14483
14580
|
}
|
|
14484
|
-
const filePath =
|
|
14485
|
-
const
|
|
14581
|
+
const filePath = path28.join(dir, file);
|
|
14582
|
+
const stat4 = await fs27.stat(filePath);
|
|
14486
14583
|
const content = await fs27.readFile(filePath, "utf-8");
|
|
14487
14584
|
const metadata = extractPolicyMetadata(content);
|
|
14488
14585
|
policies.push({
|
|
14489
14586
|
name: metadata.name || file.replace(/\.ya?ml$/, ""),
|
|
14490
14587
|
description: metadata.description || "No description",
|
|
14491
14588
|
path: file,
|
|
14492
|
-
lastModified:
|
|
14589
|
+
lastModified: stat4.mtime,
|
|
14493
14590
|
version: metadata.version || "1.0",
|
|
14494
14591
|
requiredVariables: metadata.variables || []
|
|
14495
14592
|
});
|
|
@@ -14506,7 +14603,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
|
|
|
14506
14603
|
const dir = getPoliciesDir(vaultPath2);
|
|
14507
14604
|
await ensurePoliciesDir(vaultPath2);
|
|
14508
14605
|
const filename = `${policyName}.yaml`;
|
|
14509
|
-
const filePath =
|
|
14606
|
+
const filePath = path28.join(dir, filename);
|
|
14510
14607
|
if (!overwrite) {
|
|
14511
14608
|
try {
|
|
14512
14609
|
await fs27.access(filePath);
|
|
@@ -15050,7 +15147,7 @@ import { z as z20 } from "zod";
|
|
|
15050
15147
|
|
|
15051
15148
|
// src/core/write/tagRename.ts
|
|
15052
15149
|
import * as fs28 from "fs/promises";
|
|
15053
|
-
import * as
|
|
15150
|
+
import * as path29 from "path";
|
|
15054
15151
|
import matter8 from "gray-matter";
|
|
15055
15152
|
import { getProtectedZones } from "@velvetmonkey/vault-core";
|
|
15056
15153
|
function getNotesInFolder3(index, folder) {
|
|
@@ -15156,7 +15253,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
|
|
|
15156
15253
|
const previews = [];
|
|
15157
15254
|
let totalChanges = 0;
|
|
15158
15255
|
for (const note of affectedNotes) {
|
|
15159
|
-
const fullPath =
|
|
15256
|
+
const fullPath = path29.join(vaultPath2, note.path);
|
|
15160
15257
|
let fileContent;
|
|
15161
15258
|
try {
|
|
15162
15259
|
fileContent = await fs28.readFile(fullPath, "utf-8");
|
|
@@ -15794,8 +15891,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
|
15794
15891
|
}
|
|
15795
15892
|
}
|
|
15796
15893
|
}
|
|
15797
|
-
return Array.from(noteMap.entries()).map(([
|
|
15798
|
-
path:
|
|
15894
|
+
return Array.from(noteMap.entries()).map(([path32, stats]) => ({
|
|
15895
|
+
path: path32,
|
|
15799
15896
|
access_count: stats.access_count,
|
|
15800
15897
|
last_accessed: stats.last_accessed,
|
|
15801
15898
|
tools_used: Array.from(stats.tools)
|
|
@@ -15948,7 +16045,7 @@ import { z as z25 } from "zod";
|
|
|
15948
16045
|
|
|
15949
16046
|
// src/core/read/similarity.ts
|
|
15950
16047
|
import * as fs29 from "fs";
|
|
15951
|
-
import * as
|
|
16048
|
+
import * as path30 from "path";
|
|
15952
16049
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
15953
16050
|
"the",
|
|
15954
16051
|
"be",
|
|
@@ -16085,7 +16182,7 @@ function extractKeyTerms(content, maxTerms = 15) {
|
|
|
16085
16182
|
}
|
|
16086
16183
|
function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
16087
16184
|
const limit = options.limit ?? 10;
|
|
16088
|
-
const absPath =
|
|
16185
|
+
const absPath = path30.join(vaultPath2, sourcePath);
|
|
16089
16186
|
let content;
|
|
16090
16187
|
try {
|
|
16091
16188
|
content = fs29.readFileSync(absPath, "utf-8");
|
|
@@ -16213,7 +16310,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16213
16310
|
exclude_linked: z25.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
|
|
16214
16311
|
}
|
|
16215
16312
|
},
|
|
16216
|
-
async ({ path:
|
|
16313
|
+
async ({ path: path32, limit, exclude_linked }) => {
|
|
16217
16314
|
const index = getIndex();
|
|
16218
16315
|
const vaultPath2 = getVaultPath();
|
|
16219
16316
|
const stateDb2 = getStateDb();
|
|
@@ -16222,10 +16319,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16222
16319
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
16223
16320
|
};
|
|
16224
16321
|
}
|
|
16225
|
-
if (!index.notes.has(
|
|
16322
|
+
if (!index.notes.has(path32)) {
|
|
16226
16323
|
return {
|
|
16227
16324
|
content: [{ type: "text", text: JSON.stringify({
|
|
16228
|
-
error: `Note not found: ${
|
|
16325
|
+
error: `Note not found: ${path32}`,
|
|
16229
16326
|
hint: "Use the full relative path including .md extension"
|
|
16230
16327
|
}, null, 2) }]
|
|
16231
16328
|
};
|
|
@@ -16236,12 +16333,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16236
16333
|
};
|
|
16237
16334
|
const useHybrid = hasEmbeddingsIndex();
|
|
16238
16335
|
const method = useHybrid ? "hybrid" : "bm25";
|
|
16239
|
-
const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index,
|
|
16336
|
+
const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path32, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path32, opts);
|
|
16240
16337
|
return {
|
|
16241
16338
|
content: [{
|
|
16242
16339
|
type: "text",
|
|
16243
16340
|
text: JSON.stringify({
|
|
16244
|
-
source:
|
|
16341
|
+
source: path32,
|
|
16245
16342
|
method,
|
|
16246
16343
|
exclude_linked: exclude_linked ?? true,
|
|
16247
16344
|
count: results.length,
|
|
@@ -17000,6 +17097,38 @@ async function updateEntitiesInStateDb() {
|
|
|
17000
17097
|
serverLog("index", `Failed to update entities in StateDb: ${e instanceof Error ? e.message : e}`, "error");
|
|
17001
17098
|
}
|
|
17002
17099
|
}
|
|
17100
|
+
async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
17101
|
+
const events = [];
|
|
17102
|
+
async function scanDir(dir) {
|
|
17103
|
+
let entries;
|
|
17104
|
+
try {
|
|
17105
|
+
entries = await fs30.readdir(dir, { withFileTypes: true });
|
|
17106
|
+
} catch {
|
|
17107
|
+
return;
|
|
17108
|
+
}
|
|
17109
|
+
for (const entry of entries) {
|
|
17110
|
+
const fullPath = path31.join(dir, entry.name);
|
|
17111
|
+
if (entry.isDirectory()) {
|
|
17112
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
17113
|
+
await scanDir(fullPath);
|
|
17114
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
17115
|
+
try {
|
|
17116
|
+
const stat4 = await fs30.stat(fullPath);
|
|
17117
|
+
if (stat4.mtimeMs > sinceMs) {
|
|
17118
|
+
events.push({
|
|
17119
|
+
type: "upsert",
|
|
17120
|
+
path: path31.relative(vaultPath2, fullPath),
|
|
17121
|
+
originalEvents: []
|
|
17122
|
+
});
|
|
17123
|
+
}
|
|
17124
|
+
} catch {
|
|
17125
|
+
}
|
|
17126
|
+
}
|
|
17127
|
+
}
|
|
17128
|
+
}
|
|
17129
|
+
await scanDir(vaultPath2);
|
|
17130
|
+
return events;
|
|
17131
|
+
}
|
|
17003
17132
|
async function runPostIndexWork(index) {
|
|
17004
17133
|
const postStart = Date.now();
|
|
17005
17134
|
serverLog("index", "Scanning entities...");
|
|
@@ -17092,312 +17221,500 @@ async function runPostIndexWork(index) {
|
|
|
17092
17221
|
const config = parseWatcherConfig();
|
|
17093
17222
|
const lastContentHashes = /* @__PURE__ */ new Map();
|
|
17094
17223
|
serverLog("watcher", `File watcher enabled (debounce: ${config.debounceMs}ms)`);
|
|
17095
|
-
const
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17100
|
-
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
|
|
17105
|
-
|
|
17224
|
+
const handleBatch = async (batch) => {
|
|
17225
|
+
const vaultPrefixes = /* @__PURE__ */ new Set([
|
|
17226
|
+
vaultPath.replace(/\\/g, "/"),
|
|
17227
|
+
resolvedVaultPath
|
|
17228
|
+
]);
|
|
17229
|
+
const normalizeEventPath = (rawPath) => {
|
|
17230
|
+
const normalized = rawPath.replace(/\\/g, "/");
|
|
17231
|
+
for (const prefix of vaultPrefixes) {
|
|
17232
|
+
if (normalized.startsWith(prefix + "/")) {
|
|
17233
|
+
return normalized.slice(prefix.length + 1);
|
|
17234
|
+
}
|
|
17235
|
+
}
|
|
17236
|
+
try {
|
|
17237
|
+
const resolved = realpathSync(rawPath).replace(/\\/g, "/");
|
|
17106
17238
|
for (const prefix of vaultPrefixes) {
|
|
17107
|
-
if (
|
|
17108
|
-
|
|
17109
|
-
matched = true;
|
|
17110
|
-
break;
|
|
17239
|
+
if (resolved.startsWith(prefix + "/")) {
|
|
17240
|
+
return resolved.slice(prefix.length + 1);
|
|
17111
17241
|
}
|
|
17112
17242
|
}
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
}
|
|
17122
|
-
}
|
|
17123
|
-
} catch {
|
|
17124
|
-
try {
|
|
17125
|
-
const dir = path30.dirname(event.path);
|
|
17126
|
-
const base = path30.basename(event.path);
|
|
17127
|
-
const resolvedDir = realpathSync(dir).replace(/\\/g, "/");
|
|
17128
|
-
for (const prefix of vaultPrefixes) {
|
|
17129
|
-
if (resolvedDir.startsWith(prefix + "/") || resolvedDir === prefix) {
|
|
17130
|
-
const relDir = resolvedDir === prefix ? "" : resolvedDir.slice(prefix.length + 1);
|
|
17131
|
-
event.path = relDir ? `${relDir}/${base}` : base;
|
|
17132
|
-
matched = true;
|
|
17133
|
-
break;
|
|
17134
|
-
}
|
|
17135
|
-
}
|
|
17136
|
-
} catch {
|
|
17243
|
+
} catch {
|
|
17244
|
+
try {
|
|
17245
|
+
const dir = path31.dirname(rawPath);
|
|
17246
|
+
const base = path31.basename(rawPath);
|
|
17247
|
+
const resolvedDir = realpathSync(dir).replace(/\\/g, "/");
|
|
17248
|
+
for (const prefix of vaultPrefixes) {
|
|
17249
|
+
if (resolvedDir.startsWith(prefix + "/") || resolvedDir === prefix) {
|
|
17250
|
+
const relDir = resolvedDir === prefix ? "" : resolvedDir.slice(prefix.length + 1);
|
|
17251
|
+
return relDir ? `${relDir}/${base}` : base;
|
|
17137
17252
|
}
|
|
17138
17253
|
}
|
|
17254
|
+
} catch {
|
|
17139
17255
|
}
|
|
17140
17256
|
}
|
|
17141
|
-
|
|
17142
|
-
|
|
17143
|
-
|
|
17144
|
-
|
|
17145
|
-
|
|
17257
|
+
return normalized;
|
|
17258
|
+
};
|
|
17259
|
+
for (const event of batch.events) {
|
|
17260
|
+
event.path = normalizeEventPath(event.path);
|
|
17261
|
+
}
|
|
17262
|
+
const batchRenames = (batch.renames ?? []).map((r) => ({
|
|
17263
|
+
...r,
|
|
17264
|
+
oldPath: normalizeEventPath(r.oldPath),
|
|
17265
|
+
newPath: normalizeEventPath(r.newPath)
|
|
17266
|
+
}));
|
|
17267
|
+
const filteredEvents = [];
|
|
17268
|
+
for (const event of batch.events) {
|
|
17269
|
+
if (event.type === "delete") {
|
|
17270
|
+
filteredEvents.push(event);
|
|
17271
|
+
lastContentHashes.delete(event.path);
|
|
17272
|
+
continue;
|
|
17273
|
+
}
|
|
17274
|
+
try {
|
|
17275
|
+
const content = await fs30.readFile(path31.join(vaultPath, event.path), "utf-8");
|
|
17276
|
+
const hash = createHash2("md5").update(content).digest("hex");
|
|
17277
|
+
if (lastContentHashes.get(event.path) === hash) {
|
|
17278
|
+
serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
|
|
17146
17279
|
continue;
|
|
17147
17280
|
}
|
|
17148
|
-
|
|
17149
|
-
|
|
17150
|
-
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17281
|
+
lastContentHashes.set(event.path, hash);
|
|
17282
|
+
filteredEvents.push(event);
|
|
17283
|
+
} catch {
|
|
17284
|
+
filteredEvents.push(event);
|
|
17285
|
+
}
|
|
17286
|
+
}
|
|
17287
|
+
if (batchRenames.length > 0 && stateDb) {
|
|
17288
|
+
try {
|
|
17289
|
+
const insertMove = stateDb.db.prepare(`
|
|
17290
|
+
INSERT INTO note_moves (old_path, new_path, old_folder, new_folder)
|
|
17291
|
+
VALUES (?, ?, ?, ?)
|
|
17292
|
+
`);
|
|
17293
|
+
const renameNoteLinks = stateDb.db.prepare(
|
|
17294
|
+
"UPDATE note_links SET note_path = ? WHERE note_path = ?"
|
|
17295
|
+
);
|
|
17296
|
+
const renameNoteTags = stateDb.db.prepare(
|
|
17297
|
+
"UPDATE note_tags SET note_path = ? WHERE note_path = ?"
|
|
17298
|
+
);
|
|
17299
|
+
const renameNoteLinkHistory = stateDb.db.prepare(
|
|
17300
|
+
"UPDATE note_link_history SET note_path = ? WHERE note_path = ?"
|
|
17301
|
+
);
|
|
17302
|
+
const renameWikilinkApplications = stateDb.db.prepare(
|
|
17303
|
+
"UPDATE wikilink_applications SET note_path = ? WHERE note_path = ?"
|
|
17304
|
+
);
|
|
17305
|
+
for (const rename of batchRenames) {
|
|
17306
|
+
const oldFolder = rename.oldPath.includes("/") ? rename.oldPath.split("/").slice(0, -1).join("/") : "";
|
|
17307
|
+
const newFolder = rename.newPath.includes("/") ? rename.newPath.split("/").slice(0, -1).join("/") : "";
|
|
17308
|
+
insertMove.run(rename.oldPath, rename.newPath, oldFolder || null, newFolder || null);
|
|
17309
|
+
renameNoteLinks.run(rename.newPath, rename.oldPath);
|
|
17310
|
+
renameNoteTags.run(rename.newPath, rename.oldPath);
|
|
17311
|
+
renameNoteLinkHistory.run(rename.newPath, rename.oldPath);
|
|
17312
|
+
renameWikilinkApplications.run(rename.newPath, rename.oldPath);
|
|
17313
|
+
const oldHash = lastContentHashes.get(rename.oldPath);
|
|
17314
|
+
if (oldHash !== void 0) {
|
|
17315
|
+
lastContentHashes.set(rename.newPath, oldHash);
|
|
17316
|
+
lastContentHashes.delete(rename.oldPath);
|
|
17154
17317
|
}
|
|
17155
|
-
lastContentHashes.set(event.path, hash);
|
|
17156
|
-
filteredEvents.push(event);
|
|
17157
|
-
} catch {
|
|
17158
|
-
filteredEvents.push(event);
|
|
17159
17318
|
}
|
|
17319
|
+
serverLog("watcher", `Renames: recorded ${batchRenames.length} move(s) in note_moves`);
|
|
17320
|
+
} catch (err) {
|
|
17321
|
+
serverLog("watcher", `Rename recording failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
17160
17322
|
}
|
|
17161
|
-
|
|
17323
|
+
}
|
|
17324
|
+
if (filteredEvents.length === 0) {
|
|
17325
|
+
if (batchRenames.length > 0) {
|
|
17326
|
+
serverLog("watcher", `Batch complete (renames only): ${batchRenames.length} rename(s)`);
|
|
17327
|
+
} else {
|
|
17162
17328
|
serverLog("watcher", "All files unchanged (hash gate), skipping batch");
|
|
17163
|
-
return;
|
|
17164
17329
|
}
|
|
17165
|
-
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17330
|
+
return;
|
|
17331
|
+
}
|
|
17332
|
+
serverLog("watcher", `Processing ${filteredEvents.length} file changes`);
|
|
17333
|
+
const batchStart = Date.now();
|
|
17334
|
+
const changedPaths = filteredEvents.map((e) => e.path);
|
|
17335
|
+
const tracker = createStepTracker();
|
|
17336
|
+
try {
|
|
17337
|
+
tracker.start("index_rebuild", { files_changed: filteredEvents.length, changed_paths: changedPaths });
|
|
17338
|
+
if (!vaultIndex) {
|
|
17339
|
+
vaultIndex = await buildVaultIndex(vaultPath);
|
|
17340
|
+
serverLog("watcher", `Index rebuilt (full): ${vaultIndex.notes.size} notes, ${vaultIndex.entities.size} entities`);
|
|
17341
|
+
} else {
|
|
17342
|
+
const absoluteBatch = {
|
|
17343
|
+
...batch,
|
|
17344
|
+
events: filteredEvents.map((e) => ({
|
|
17345
|
+
...e,
|
|
17346
|
+
path: path31.join(vaultPath, e.path)
|
|
17347
|
+
}))
|
|
17348
|
+
};
|
|
17349
|
+
const batchResult = await processBatch(vaultIndex, vaultPath, absoluteBatch);
|
|
17350
|
+
serverLog("watcher", `Incremental: ${batchResult.successful}/${batchResult.total} files in ${batchResult.durationMs}ms`);
|
|
17351
|
+
}
|
|
17352
|
+
setIndexState("ready");
|
|
17353
|
+
tracker.end({ note_count: vaultIndex.notes.size, entity_count: vaultIndex.entities.size, tag_count: vaultIndex.tags.size });
|
|
17354
|
+
tracker.start("note_moves", { count: batchRenames.length });
|
|
17355
|
+
tracker.end({
|
|
17356
|
+
renames: batchRenames.map((r) => ({ oldPath: r.oldPath, newPath: r.newPath }))
|
|
17357
|
+
});
|
|
17358
|
+
if (batchRenames.length > 0) {
|
|
17359
|
+
serverLog("watcher", `Note moves: ${batchRenames.length} rename(s) recorded`);
|
|
17360
|
+
}
|
|
17361
|
+
const hubBefore = /* @__PURE__ */ new Map();
|
|
17362
|
+
if (stateDb) {
|
|
17363
|
+
const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
|
|
17364
|
+
for (const r of rows) hubBefore.set(r.name, r.hub_score);
|
|
17365
|
+
}
|
|
17366
|
+
const entitiesBefore = stateDb ? getAllEntitiesFromDb3(stateDb) : [];
|
|
17367
|
+
tracker.start("entity_scan", { note_count: vaultIndex.notes.size });
|
|
17368
|
+
await updateEntitiesInStateDb();
|
|
17369
|
+
const entitiesAfter = stateDb ? getAllEntitiesFromDb3(stateDb) : [];
|
|
17370
|
+
const entityDiff = computeEntityDiff(entitiesBefore, entitiesAfter);
|
|
17371
|
+
const categoryChanges = [];
|
|
17372
|
+
if (stateDb) {
|
|
17373
|
+
const beforeMap = new Map(entitiesBefore.map((e) => [e.name, e]));
|
|
17374
|
+
const insertChange = stateDb.db.prepare(
|
|
17375
|
+
"INSERT INTO entity_changes (entity, field, old_value, new_value) VALUES (?, ?, ?, ?)"
|
|
17376
|
+
);
|
|
17377
|
+
for (const after of entitiesAfter) {
|
|
17378
|
+
const before = beforeMap.get(after.name);
|
|
17379
|
+
if (before && before.category !== after.category) {
|
|
17380
|
+
insertChange.run(after.name, "category", before.category, after.category);
|
|
17381
|
+
categoryChanges.push({ entity: after.name, from: before.category, to: after.category });
|
|
17382
|
+
}
|
|
17383
|
+
}
|
|
17384
|
+
}
|
|
17385
|
+
tracker.end({ entity_count: entitiesAfter.length, ...entityDiff, category_changes: categoryChanges });
|
|
17386
|
+
serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
|
|
17387
|
+
tracker.start("hub_scores", { entity_count: entitiesAfter.length });
|
|
17388
|
+
const hubUpdated = await exportHubScores(vaultIndex, stateDb);
|
|
17389
|
+
const hubDiffs = [];
|
|
17390
|
+
if (stateDb) {
|
|
17391
|
+
const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
|
|
17392
|
+
for (const r of rows) {
|
|
17393
|
+
const prev = hubBefore.get(r.name) ?? 0;
|
|
17394
|
+
if (prev !== r.hub_score) hubDiffs.push({ entity: r.name, before: prev, after: r.hub_score });
|
|
17395
|
+
}
|
|
17396
|
+
}
|
|
17397
|
+
tracker.end({ updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) });
|
|
17398
|
+
serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
|
|
17399
|
+
tracker.start("recency", { entity_count: entitiesAfter.length });
|
|
17169
17400
|
try {
|
|
17170
|
-
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
|
|
17401
|
+
const cachedRecency = loadRecencyFromStateDb();
|
|
17402
|
+
const cacheAgeMs = cachedRecency ? Date.now() - (cachedRecency.lastUpdated ?? 0) : Infinity;
|
|
17403
|
+
if (cacheAgeMs >= 60 * 60 * 1e3) {
|
|
17404
|
+
const entities = entitiesAfter.map((e) => ({ name: e.name, path: e.path, aliases: e.aliases }));
|
|
17405
|
+
const recencyIndex2 = await buildRecencyIndex(vaultPath, entities);
|
|
17406
|
+
saveRecencyToStateDb(recencyIndex2);
|
|
17407
|
+
tracker.end({ rebuilt: true, entities: recencyIndex2.lastMentioned.size });
|
|
17408
|
+
serverLog("watcher", `Recency: rebuilt ${recencyIndex2.lastMentioned.size} entities`);
|
|
17174
17409
|
} else {
|
|
17175
|
-
|
|
17176
|
-
|
|
17177
|
-
events: filteredEvents.map((e) => ({
|
|
17178
|
-
...e,
|
|
17179
|
-
path: path30.join(vaultPath, e.path)
|
|
17180
|
-
}))
|
|
17181
|
-
};
|
|
17182
|
-
const batchResult = await processBatch(vaultIndex, vaultPath, absoluteBatch);
|
|
17183
|
-
serverLog("watcher", `Incremental: ${batchResult.successful}/${batchResult.total} files in ${batchResult.durationMs}ms`);
|
|
17410
|
+
tracker.end({ rebuilt: false, cached_age_ms: cacheAgeMs });
|
|
17411
|
+
serverLog("watcher", `Recency: cache valid (${Math.round(cacheAgeMs / 1e3)}s old)`);
|
|
17184
17412
|
}
|
|
17185
|
-
|
|
17186
|
-
tracker.end({
|
|
17187
|
-
|
|
17188
|
-
|
|
17189
|
-
|
|
17190
|
-
|
|
17413
|
+
} catch (e) {
|
|
17414
|
+
tracker.end({ error: String(e) });
|
|
17415
|
+
serverLog("watcher", `Recency: failed: ${e}`);
|
|
17416
|
+
}
|
|
17417
|
+
if (hasEmbeddingsIndex()) {
|
|
17418
|
+
tracker.start("note_embeddings", { files: filteredEvents.length });
|
|
17419
|
+
let embUpdated = 0;
|
|
17420
|
+
let embRemoved = 0;
|
|
17421
|
+
for (const event of filteredEvents) {
|
|
17422
|
+
try {
|
|
17423
|
+
if (event.type === "delete") {
|
|
17424
|
+
removeEmbedding(event.path);
|
|
17425
|
+
embRemoved++;
|
|
17426
|
+
} else if (event.path.endsWith(".md")) {
|
|
17427
|
+
const absPath = path31.join(vaultPath, event.path);
|
|
17428
|
+
await updateEmbedding(event.path, absPath);
|
|
17429
|
+
embUpdated++;
|
|
17430
|
+
}
|
|
17431
|
+
} catch {
|
|
17432
|
+
}
|
|
17191
17433
|
}
|
|
17192
|
-
|
|
17193
|
-
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17200
|
-
const
|
|
17201
|
-
|
|
17202
|
-
|
|
17203
|
-
|
|
17204
|
-
|
|
17205
|
-
const
|
|
17206
|
-
|
|
17434
|
+
tracker.end({ updated: embUpdated, removed: embRemoved });
|
|
17435
|
+
serverLog("watcher", `Note embeddings: ${embUpdated} updated, ${embRemoved} removed`);
|
|
17436
|
+
} else {
|
|
17437
|
+
tracker.skip("note_embeddings", "not built");
|
|
17438
|
+
}
|
|
17439
|
+
if (hasEntityEmbeddingsIndex() && stateDb) {
|
|
17440
|
+
tracker.start("entity_embeddings", { files: filteredEvents.length });
|
|
17441
|
+
let entEmbUpdated = 0;
|
|
17442
|
+
const entEmbNames = [];
|
|
17443
|
+
try {
|
|
17444
|
+
const allEntities = getAllEntitiesFromDb3(stateDb);
|
|
17445
|
+
for (const event of filteredEvents) {
|
|
17446
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
17447
|
+
const matching = allEntities.filter((e) => e.path === event.path);
|
|
17448
|
+
for (const entity of matching) {
|
|
17449
|
+
await updateEntityEmbedding(entity.name, {
|
|
17450
|
+
name: entity.name,
|
|
17451
|
+
path: entity.path,
|
|
17452
|
+
category: entity.category,
|
|
17453
|
+
aliases: entity.aliases
|
|
17454
|
+
}, vaultPath);
|
|
17455
|
+
entEmbUpdated++;
|
|
17456
|
+
entEmbNames.push(entity.name);
|
|
17457
|
+
}
|
|
17207
17458
|
}
|
|
17459
|
+
} catch {
|
|
17208
17460
|
}
|
|
17209
|
-
tracker.end({ updated:
|
|
17210
|
-
serverLog("watcher", `
|
|
17211
|
-
|
|
17461
|
+
tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10) });
|
|
17462
|
+
serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated`);
|
|
17463
|
+
} else {
|
|
17464
|
+
tracker.skip("entity_embeddings", !stateDb ? "no stateDb" : "not built");
|
|
17465
|
+
}
|
|
17466
|
+
if (stateDb) {
|
|
17467
|
+
tracker.start("index_cache", { note_count: vaultIndex.notes.size });
|
|
17212
17468
|
try {
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
|
|
17216
|
-
|
|
17217
|
-
|
|
17218
|
-
|
|
17219
|
-
|
|
17220
|
-
|
|
17221
|
-
|
|
17222
|
-
|
|
17223
|
-
|
|
17469
|
+
saveVaultIndexToCache(stateDb, vaultIndex);
|
|
17470
|
+
tracker.end({ saved: true });
|
|
17471
|
+
serverLog("watcher", "Index cache saved");
|
|
17472
|
+
} catch (err) {
|
|
17473
|
+
tracker.end({ saved: false, error: err instanceof Error ? err.message : String(err) });
|
|
17474
|
+
serverLog("index", `Failed to update index cache: ${err instanceof Error ? err.message : err}`, "error");
|
|
17475
|
+
}
|
|
17476
|
+
} else {
|
|
17477
|
+
tracker.skip("index_cache", "no stateDb");
|
|
17478
|
+
}
|
|
17479
|
+
tracker.start("task_cache", { files: filteredEvents.length });
|
|
17480
|
+
let taskUpdated = 0;
|
|
17481
|
+
let taskRemoved = 0;
|
|
17482
|
+
for (const event of filteredEvents) {
|
|
17483
|
+
try {
|
|
17484
|
+
if (event.type === "delete") {
|
|
17485
|
+
removeTaskCacheForFile(event.path);
|
|
17486
|
+
taskRemoved++;
|
|
17487
|
+
} else if (event.path.endsWith(".md")) {
|
|
17488
|
+
await updateTaskCacheForFile(vaultPath, event.path);
|
|
17489
|
+
taskUpdated++;
|
|
17224
17490
|
}
|
|
17225
|
-
} catch
|
|
17226
|
-
tracker.end({ error: String(e) });
|
|
17227
|
-
serverLog("watcher", `Recency: failed: ${e}`);
|
|
17491
|
+
} catch {
|
|
17228
17492
|
}
|
|
17229
|
-
|
|
17230
|
-
|
|
17231
|
-
|
|
17232
|
-
|
|
17233
|
-
|
|
17234
|
-
|
|
17235
|
-
|
|
17236
|
-
|
|
17237
|
-
|
|
17238
|
-
|
|
17239
|
-
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17244
|
-
|
|
17493
|
+
}
|
|
17494
|
+
tracker.end({ updated: taskUpdated, removed: taskRemoved });
|
|
17495
|
+
serverLog("watcher", `Task cache: ${taskUpdated} updated, ${taskRemoved} removed`);
|
|
17496
|
+
tracker.start("forward_links", { files: filteredEvents.length });
|
|
17497
|
+
const eventTypeMap = new Map(filteredEvents.map((e) => [e.path, e.type]));
|
|
17498
|
+
const forwardLinkResults = [];
|
|
17499
|
+
let totalResolved = 0;
|
|
17500
|
+
let totalDead = 0;
|
|
17501
|
+
for (const event of filteredEvents) {
|
|
17502
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
17503
|
+
try {
|
|
17504
|
+
const links = getForwardLinksForNote(vaultIndex, event.path);
|
|
17505
|
+
const resolved = [];
|
|
17506
|
+
const dead = [];
|
|
17507
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17508
|
+
for (const link of links) {
|
|
17509
|
+
const name = link.target;
|
|
17510
|
+
if (seen.has(name.toLowerCase())) continue;
|
|
17511
|
+
seen.add(name.toLowerCase());
|
|
17512
|
+
if (link.exists) resolved.push(name);
|
|
17513
|
+
else dead.push(name);
|
|
17245
17514
|
}
|
|
17246
|
-
|
|
17247
|
-
|
|
17248
|
-
|
|
17249
|
-
|
|
17515
|
+
if (resolved.length > 0 || dead.length > 0) {
|
|
17516
|
+
forwardLinkResults.push({ file: event.path, resolved, dead });
|
|
17517
|
+
}
|
|
17518
|
+
totalResolved += resolved.length;
|
|
17519
|
+
totalDead += dead.length;
|
|
17520
|
+
} catch {
|
|
17250
17521
|
}
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
|
|
17254
|
-
|
|
17255
|
-
|
|
17256
|
-
|
|
17257
|
-
|
|
17258
|
-
|
|
17259
|
-
|
|
17260
|
-
|
|
17261
|
-
|
|
17262
|
-
|
|
17263
|
-
|
|
17264
|
-
|
|
17265
|
-
|
|
17266
|
-
|
|
17267
|
-
|
|
17268
|
-
|
|
17522
|
+
}
|
|
17523
|
+
const linkDiffs = [];
|
|
17524
|
+
if (stateDb) {
|
|
17525
|
+
const upsertHistory = stateDb.db.prepare(`
|
|
17526
|
+
INSERT INTO note_link_history (note_path, target) VALUES (?, ?)
|
|
17527
|
+
ON CONFLICT(note_path, target) DO UPDATE SET edits_survived = edits_survived + 1
|
|
17528
|
+
`);
|
|
17529
|
+
const checkThreshold = stateDb.db.prepare(`
|
|
17530
|
+
SELECT target FROM note_link_history
|
|
17531
|
+
WHERE note_path = ? AND target = ? AND edits_survived >= 3 AND last_positive_at IS NULL
|
|
17532
|
+
`);
|
|
17533
|
+
const markPositive = stateDb.db.prepare(`
|
|
17534
|
+
UPDATE note_link_history SET last_positive_at = datetime('now') WHERE note_path = ? AND target = ?
|
|
17535
|
+
`);
|
|
17536
|
+
for (const entry of forwardLinkResults) {
|
|
17537
|
+
const currentSet = /* @__PURE__ */ new Set([
|
|
17538
|
+
...entry.resolved.map((n) => n.toLowerCase()),
|
|
17539
|
+
...entry.dead.map((n) => n.toLowerCase())
|
|
17540
|
+
]);
|
|
17541
|
+
const previousSet = getStoredNoteLinks(stateDb, entry.file);
|
|
17542
|
+
if (previousSet.size === 0) {
|
|
17543
|
+
updateStoredNoteLinks(stateDb, entry.file, currentSet);
|
|
17544
|
+
continue;
|
|
17545
|
+
}
|
|
17546
|
+
const diff = diffNoteLinks(previousSet, currentSet);
|
|
17547
|
+
if (diff.added.length > 0 || diff.removed.length > 0) {
|
|
17548
|
+
linkDiffs.push({ file: entry.file, ...diff });
|
|
17549
|
+
}
|
|
17550
|
+
updateStoredNoteLinks(stateDb, entry.file, currentSet);
|
|
17551
|
+
for (const link of currentSet) {
|
|
17552
|
+
if (!previousSet.has(link)) continue;
|
|
17553
|
+
upsertHistory.run(entry.file, link);
|
|
17554
|
+
const hit = checkThreshold.get(entry.file, link);
|
|
17555
|
+
if (hit) {
|
|
17556
|
+
const entity = entitiesAfter.find(
|
|
17557
|
+
(e) => e.nameLower === link || (e.aliases ?? []).some((a) => a.toLowerCase() === link)
|
|
17558
|
+
);
|
|
17559
|
+
if (entity) {
|
|
17560
|
+
recordFeedback(stateDb, entity.name, "implicit:kept", entry.file, true);
|
|
17561
|
+
markPositive.run(entry.file, link);
|
|
17269
17562
|
}
|
|
17270
17563
|
}
|
|
17271
|
-
} catch {
|
|
17272
17564
|
}
|
|
17273
|
-
tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10) });
|
|
17274
|
-
serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated`);
|
|
17275
|
-
} else {
|
|
17276
|
-
tracker.skip("entity_embeddings", !stateDb ? "no stateDb" : "not built");
|
|
17277
17565
|
}
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
tracker.end({ saved: false, error: err instanceof Error ? err.message : String(err) });
|
|
17286
|
-
serverLog("index", `Failed to update index cache: ${err instanceof Error ? err.message : err}`, "error");
|
|
17566
|
+
for (const event of filteredEvents) {
|
|
17567
|
+
if (event.type === "delete") {
|
|
17568
|
+
const previousSet = getStoredNoteLinks(stateDb, event.path);
|
|
17569
|
+
if (previousSet.size > 0) {
|
|
17570
|
+
linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
|
|
17571
|
+
updateStoredNoteLinks(stateDb, event.path, /* @__PURE__ */ new Set());
|
|
17572
|
+
}
|
|
17287
17573
|
}
|
|
17288
|
-
} else {
|
|
17289
|
-
tracker.skip("index_cache", "no stateDb");
|
|
17290
17574
|
}
|
|
17291
|
-
|
|
17292
|
-
|
|
17293
|
-
|
|
17575
|
+
}
|
|
17576
|
+
tracker.end({
|
|
17577
|
+
total_resolved: totalResolved,
|
|
17578
|
+
total_dead: totalDead,
|
|
17579
|
+
links: forwardLinkResults,
|
|
17580
|
+
link_diffs: linkDiffs
|
|
17581
|
+
});
|
|
17582
|
+
serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead`);
|
|
17583
|
+
tracker.start("wikilink_check", { files: filteredEvents.length });
|
|
17584
|
+
const trackedLinks = [];
|
|
17585
|
+
if (stateDb) {
|
|
17294
17586
|
for (const event of filteredEvents) {
|
|
17587
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
17295
17588
|
try {
|
|
17296
|
-
|
|
17297
|
-
|
|
17298
|
-
taskRemoved++;
|
|
17299
|
-
} else if (event.path.endsWith(".md")) {
|
|
17300
|
-
await updateTaskCacheForFile(vaultPath, event.path);
|
|
17301
|
-
taskUpdated++;
|
|
17302
|
-
}
|
|
17589
|
+
const apps = getTrackedApplications(stateDb, event.path);
|
|
17590
|
+
if (apps.length > 0) trackedLinks.push({ file: event.path, entities: apps });
|
|
17303
17591
|
} catch {
|
|
17304
17592
|
}
|
|
17305
17593
|
}
|
|
17306
|
-
|
|
17307
|
-
|
|
17308
|
-
|
|
17309
|
-
const
|
|
17310
|
-
|
|
17311
|
-
|
|
17594
|
+
}
|
|
17595
|
+
for (const diff of linkDiffs) {
|
|
17596
|
+
if (diff.added.length === 0) continue;
|
|
17597
|
+
const existing2 = trackedLinks.find((t) => t.file === diff.file);
|
|
17598
|
+
if (existing2) {
|
|
17599
|
+
const set = new Set(existing2.entities.map((e) => e.toLowerCase()));
|
|
17600
|
+
for (const a of diff.added) {
|
|
17601
|
+
if (!set.has(a)) {
|
|
17602
|
+
existing2.entities.push(a);
|
|
17603
|
+
set.add(a);
|
|
17604
|
+
}
|
|
17605
|
+
}
|
|
17606
|
+
} else {
|
|
17607
|
+
trackedLinks.push({ file: diff.file, entities: diff.added });
|
|
17608
|
+
}
|
|
17609
|
+
}
|
|
17610
|
+
tracker.end({ tracked: trackedLinks });
|
|
17611
|
+
serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files`);
|
|
17612
|
+
tracker.start("implicit_feedback", { files: filteredEvents.length });
|
|
17613
|
+
const feedbackResults = [];
|
|
17614
|
+
if (stateDb) {
|
|
17312
17615
|
for (const event of filteredEvents) {
|
|
17313
17616
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
17314
17617
|
try {
|
|
17315
|
-
const
|
|
17316
|
-
const
|
|
17317
|
-
const
|
|
17318
|
-
const seen = /* @__PURE__ */ new Set();
|
|
17319
|
-
for (const link of links) {
|
|
17320
|
-
const name = link.target;
|
|
17321
|
-
if (seen.has(name.toLowerCase())) continue;
|
|
17322
|
-
seen.add(name.toLowerCase());
|
|
17323
|
-
if (link.exists) resolved.push(name);
|
|
17324
|
-
else dead.push(name);
|
|
17325
|
-
}
|
|
17326
|
-
if (resolved.length > 0 || dead.length > 0) {
|
|
17327
|
-
forwardLinkResults.push({ file: event.path, resolved, dead });
|
|
17328
|
-
}
|
|
17329
|
-
totalResolved += resolved.length;
|
|
17330
|
-
totalDead += dead.length;
|
|
17618
|
+
const content = await fs30.readFile(path31.join(vaultPath, event.path), "utf-8");
|
|
17619
|
+
const removed = processImplicitFeedback(stateDb, event.path, content);
|
|
17620
|
+
for (const entity of removed) feedbackResults.push({ entity, file: event.path });
|
|
17331
17621
|
} catch {
|
|
17332
17622
|
}
|
|
17333
17623
|
}
|
|
17334
|
-
|
|
17335
|
-
|
|
17336
|
-
|
|
17337
|
-
|
|
17338
|
-
|
|
17339
|
-
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
|
|
17343
|
-
|
|
17344
|
-
|
|
17345
|
-
try {
|
|
17346
|
-
const apps = getTrackedApplications(stateDb, event.path);
|
|
17347
|
-
if (apps.length > 0) trackedLinks.push({ file: event.path, entities: apps });
|
|
17348
|
-
} catch {
|
|
17624
|
+
}
|
|
17625
|
+
if (stateDb && linkDiffs.length > 0) {
|
|
17626
|
+
for (const diff of linkDiffs) {
|
|
17627
|
+
for (const target of diff.removed) {
|
|
17628
|
+
if (feedbackResults.some((r) => r.entity === target && r.file === diff.file)) continue;
|
|
17629
|
+
const entity = entitiesAfter.find(
|
|
17630
|
+
(e) => e.nameLower === target || (e.aliases ?? []).some((a) => a.toLowerCase() === target)
|
|
17631
|
+
);
|
|
17632
|
+
if (entity) {
|
|
17633
|
+
recordFeedback(stateDb, entity.name, "implicit:removed", diff.file, false);
|
|
17634
|
+
feedbackResults.push({ entity: entity.name, file: diff.file });
|
|
17349
17635
|
}
|
|
17350
17636
|
}
|
|
17351
17637
|
}
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
|
|
17359
|
-
|
|
17360
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17638
|
+
}
|
|
17639
|
+
tracker.end({ removals: feedbackResults });
|
|
17640
|
+
if (feedbackResults.length > 0) {
|
|
17641
|
+
serverLog("watcher", `Implicit feedback: ${feedbackResults.length} removals detected`);
|
|
17642
|
+
}
|
|
17643
|
+
tracker.start("tag_scan", { files: filteredEvents.length });
|
|
17644
|
+
const tagDiffs = [];
|
|
17645
|
+
if (stateDb) {
|
|
17646
|
+
const noteTagsForward = /* @__PURE__ */ new Map();
|
|
17647
|
+
for (const [tag, paths] of vaultIndex.tags) {
|
|
17648
|
+
for (const notePath of paths) {
|
|
17649
|
+
if (!noteTagsForward.has(notePath)) noteTagsForward.set(notePath, /* @__PURE__ */ new Set());
|
|
17650
|
+
noteTagsForward.get(notePath).add(tag);
|
|
17365
17651
|
}
|
|
17366
17652
|
}
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17653
|
+
for (const event of filteredEvents) {
|
|
17654
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
17655
|
+
const currentSet = noteTagsForward.get(event.path) ?? /* @__PURE__ */ new Set();
|
|
17656
|
+
const previousSet = getStoredNoteTags(stateDb, event.path);
|
|
17657
|
+
if (previousSet.size === 0 && currentSet.size > 0) {
|
|
17658
|
+
updateStoredNoteTags(stateDb, event.path, currentSet);
|
|
17659
|
+
continue;
|
|
17660
|
+
}
|
|
17661
|
+
const added = [...currentSet].filter((t) => !previousSet.has(t));
|
|
17662
|
+
const removed = [...previousSet].filter((t) => !currentSet.has(t));
|
|
17663
|
+
if (added.length > 0 || removed.length > 0) {
|
|
17664
|
+
tagDiffs.push({ file: event.path, added, removed });
|
|
17665
|
+
}
|
|
17666
|
+
updateStoredNoteTags(stateDb, event.path, currentSet);
|
|
17381
17667
|
}
|
|
17382
|
-
|
|
17383
|
-
|
|
17384
|
-
|
|
17385
|
-
|
|
17386
|
-
|
|
17387
|
-
|
|
17388
|
-
|
|
17389
|
-
|
|
17390
|
-
duration_ms: duration,
|
|
17391
|
-
success: false,
|
|
17392
|
-
files_changed: filteredEvents.length,
|
|
17393
|
-
changed_paths: changedPaths,
|
|
17394
|
-
error: err instanceof Error ? err.message : String(err),
|
|
17395
|
-
steps: tracker.steps
|
|
17396
|
-
});
|
|
17668
|
+
for (const event of filteredEvents) {
|
|
17669
|
+
if (event.type === "delete") {
|
|
17670
|
+
const previousSet = getStoredNoteTags(stateDb, event.path);
|
|
17671
|
+
if (previousSet.size > 0) {
|
|
17672
|
+
tagDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
|
|
17673
|
+
updateStoredNoteTags(stateDb, event.path, /* @__PURE__ */ new Set());
|
|
17674
|
+
}
|
|
17675
|
+
}
|
|
17397
17676
|
}
|
|
17398
|
-
serverLog("watcher", `Failed to rebuild index: ${err instanceof Error ? err.message : err}`, "error");
|
|
17399
17677
|
}
|
|
17400
|
-
|
|
17678
|
+
const totalTagsAdded = tagDiffs.reduce((s, d) => s + d.added.length, 0);
|
|
17679
|
+
const totalTagsRemoved = tagDiffs.reduce((s, d) => s + d.removed.length, 0);
|
|
17680
|
+
tracker.end({ total_added: totalTagsAdded, total_removed: totalTagsRemoved, tag_diffs: tagDiffs });
|
|
17681
|
+
if (tagDiffs.length > 0) {
|
|
17682
|
+
serverLog("watcher", `Tag scan: ${totalTagsAdded} added, ${totalTagsRemoved} removed across ${tagDiffs.length} files`);
|
|
17683
|
+
}
|
|
17684
|
+
const duration = Date.now() - batchStart;
|
|
17685
|
+
if (stateDb) {
|
|
17686
|
+
recordIndexEvent(stateDb, {
|
|
17687
|
+
trigger: "watcher",
|
|
17688
|
+
duration_ms: duration,
|
|
17689
|
+
note_count: vaultIndex.notes.size,
|
|
17690
|
+
files_changed: filteredEvents.length,
|
|
17691
|
+
changed_paths: changedPaths,
|
|
17692
|
+
steps: tracker.steps
|
|
17693
|
+
});
|
|
17694
|
+
}
|
|
17695
|
+
serverLog("watcher", `Batch complete: ${filteredEvents.length} files, ${duration}ms, ${tracker.steps.length} steps`);
|
|
17696
|
+
} catch (err) {
|
|
17697
|
+
setIndexState("error");
|
|
17698
|
+
setIndexError(err instanceof Error ? err : new Error(String(err)));
|
|
17699
|
+
const duration = Date.now() - batchStart;
|
|
17700
|
+
if (stateDb) {
|
|
17701
|
+
recordIndexEvent(stateDb, {
|
|
17702
|
+
trigger: "watcher",
|
|
17703
|
+
duration_ms: duration,
|
|
17704
|
+
success: false,
|
|
17705
|
+
files_changed: filteredEvents.length,
|
|
17706
|
+
changed_paths: changedPaths,
|
|
17707
|
+
error: err instanceof Error ? err.message : String(err),
|
|
17708
|
+
steps: tracker.steps
|
|
17709
|
+
});
|
|
17710
|
+
}
|
|
17711
|
+
serverLog("watcher", `Failed to rebuild index: ${err instanceof Error ? err.message : err}`, "error");
|
|
17712
|
+
}
|
|
17713
|
+
};
|
|
17714
|
+
const watcher = createVaultWatcher({
|
|
17715
|
+
vaultPath,
|
|
17716
|
+
config,
|
|
17717
|
+
onBatch: handleBatch,
|
|
17401
17718
|
onStateChange: (status) => {
|
|
17402
17719
|
if (status.state === "dirty") {
|
|
17403
17720
|
serverLog("watcher", "Index may be stale", "warn");
|
|
@@ -17407,6 +17724,16 @@ async function runPostIndexWork(index) {
|
|
|
17407
17724
|
serverLog("watcher", `Watcher error: ${err.message}`, "error");
|
|
17408
17725
|
}
|
|
17409
17726
|
});
|
|
17727
|
+
if (stateDb) {
|
|
17728
|
+
const lastPipelineEvent = getRecentPipelineEvent(stateDb);
|
|
17729
|
+
if (lastPipelineEvent) {
|
|
17730
|
+
const catchupEvents = await buildStartupCatchupBatch(vaultPath, lastPipelineEvent.timestamp);
|
|
17731
|
+
if (catchupEvents.length > 0) {
|
|
17732
|
+
console.error(`[Flywheel] Startup catch-up: ${catchupEvents.length} file(s) modified while offline`);
|
|
17733
|
+
await handleBatch({ events: catchupEvents, renames: [], timestamp: Date.now() });
|
|
17734
|
+
}
|
|
17735
|
+
}
|
|
17736
|
+
}
|
|
17410
17737
|
watcher.start();
|
|
17411
17738
|
serverLog("watcher", "File watcher started");
|
|
17412
17739
|
}
|