@velvetmonkey/flywheel-memory 2.0.35 → 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 +988 -419
- 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
|
}
|
|
@@ -3023,6 +3075,208 @@ function parseWatcherConfig() {
|
|
|
3023
3075
|
};
|
|
3024
3076
|
}
|
|
3025
3077
|
|
|
3078
|
+
// src/core/read/watch/incrementalIndex.ts
|
|
3079
|
+
import path7 from "path";
|
|
3080
|
+
function normalizeTarget2(target) {
|
|
3081
|
+
return target.toLowerCase().replace(/\.md$/, "");
|
|
3082
|
+
}
|
|
3083
|
+
function normalizeNotePath2(notePath) {
|
|
3084
|
+
return notePath.toLowerCase().replace(/\.md$/, "");
|
|
3085
|
+
}
|
|
3086
|
+
function removeNoteFromIndex(index, notePath) {
|
|
3087
|
+
const note = index.notes.get(notePath);
|
|
3088
|
+
if (!note) {
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
index.notes.delete(notePath);
|
|
3092
|
+
const normalizedTitle = normalizeTarget2(note.title);
|
|
3093
|
+
const normalizedPath = normalizeNotePath2(notePath);
|
|
3094
|
+
if (index.entities.get(normalizedTitle) === notePath) {
|
|
3095
|
+
index.entities.delete(normalizedTitle);
|
|
3096
|
+
}
|
|
3097
|
+
if (index.entities.get(normalizedPath) === notePath) {
|
|
3098
|
+
index.entities.delete(normalizedPath);
|
|
3099
|
+
}
|
|
3100
|
+
for (const alias of note.aliases) {
|
|
3101
|
+
const normalizedAlias = normalizeTarget2(alias);
|
|
3102
|
+
if (index.entities.get(normalizedAlias) === notePath) {
|
|
3103
|
+
index.entities.delete(normalizedAlias);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
for (const tag of note.tags) {
|
|
3107
|
+
const tagPaths = index.tags.get(tag);
|
|
3108
|
+
if (tagPaths) {
|
|
3109
|
+
tagPaths.delete(notePath);
|
|
3110
|
+
if (tagPaths.size === 0) {
|
|
3111
|
+
index.tags.delete(tag);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
for (const link of note.outlinks) {
|
|
3116
|
+
const normalizedTarget = normalizeTarget2(link.target);
|
|
3117
|
+
const targetPath = index.entities.get(normalizedTarget);
|
|
3118
|
+
const key = targetPath ? normalizeNotePath2(targetPath) : normalizedTarget;
|
|
3119
|
+
const backlinks = index.backlinks.get(key);
|
|
3120
|
+
if (backlinks) {
|
|
3121
|
+
const filtered = backlinks.filter((bl) => bl.source !== notePath);
|
|
3122
|
+
if (filtered.length === 0) {
|
|
3123
|
+
index.backlinks.delete(key);
|
|
3124
|
+
} else {
|
|
3125
|
+
index.backlinks.set(key, filtered);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return true;
|
|
3130
|
+
}
|
|
3131
|
+
function addNoteToIndex(index, note) {
|
|
3132
|
+
index.notes.set(note.path, note);
|
|
3133
|
+
const normalizedTitle = normalizeTarget2(note.title);
|
|
3134
|
+
const normalizedPath = normalizeNotePath2(note.path);
|
|
3135
|
+
if (!index.entities.has(normalizedTitle)) {
|
|
3136
|
+
index.entities.set(normalizedTitle, note.path);
|
|
3137
|
+
}
|
|
3138
|
+
index.entities.set(normalizedPath, note.path);
|
|
3139
|
+
for (const alias of note.aliases) {
|
|
3140
|
+
const normalizedAlias = normalizeTarget2(alias);
|
|
3141
|
+
if (!index.entities.has(normalizedAlias)) {
|
|
3142
|
+
index.entities.set(normalizedAlias, note.path);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
for (const tag of note.tags) {
|
|
3146
|
+
if (!index.tags.has(tag)) {
|
|
3147
|
+
index.tags.set(tag, /* @__PURE__ */ new Set());
|
|
3148
|
+
}
|
|
3149
|
+
index.tags.get(tag).add(note.path);
|
|
3150
|
+
}
|
|
3151
|
+
for (const link of note.outlinks) {
|
|
3152
|
+
const normalizedTarget = normalizeTarget2(link.target);
|
|
3153
|
+
const targetPath = index.entities.get(normalizedTarget);
|
|
3154
|
+
const key = targetPath ? normalizeNotePath2(targetPath) : normalizedTarget;
|
|
3155
|
+
if (!index.backlinks.has(key)) {
|
|
3156
|
+
index.backlinks.set(key, []);
|
|
3157
|
+
}
|
|
3158
|
+
index.backlinks.get(key).push({
|
|
3159
|
+
source: note.path,
|
|
3160
|
+
line: link.line
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
async function upsertNote(index, vaultPath2, notePath) {
|
|
3165
|
+
try {
|
|
3166
|
+
const existed = index.notes.has(notePath);
|
|
3167
|
+
if (existed) {
|
|
3168
|
+
removeNoteFromIndex(index, notePath);
|
|
3169
|
+
}
|
|
3170
|
+
const fullPath = path7.join(vaultPath2, notePath);
|
|
3171
|
+
const fs31 = await import("fs/promises");
|
|
3172
|
+
const stats = await fs31.stat(fullPath);
|
|
3173
|
+
const vaultFile = {
|
|
3174
|
+
path: notePath,
|
|
3175
|
+
absolutePath: fullPath,
|
|
3176
|
+
modified: stats.mtime
|
|
3177
|
+
};
|
|
3178
|
+
const note = await parseNote(vaultFile);
|
|
3179
|
+
addNoteToIndex(index, note);
|
|
3180
|
+
return {
|
|
3181
|
+
success: true,
|
|
3182
|
+
action: existed ? "updated" : "added",
|
|
3183
|
+
path: notePath
|
|
3184
|
+
};
|
|
3185
|
+
} catch (error) {
|
|
3186
|
+
return {
|
|
3187
|
+
success: false,
|
|
3188
|
+
action: "unchanged",
|
|
3189
|
+
path: notePath,
|
|
3190
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
function deleteNote(index, notePath) {
|
|
3195
|
+
const removed = removeNoteFromIndex(index, notePath);
|
|
3196
|
+
return {
|
|
3197
|
+
success: removed,
|
|
3198
|
+
action: removed ? "removed" : "unchanged",
|
|
3199
|
+
path: notePath
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// src/core/read/watch/batchProcessor.ts
|
|
3204
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
3205
|
+
var YIELD_INTERVAL = 10;
|
|
3206
|
+
async function processBatch(index, vaultPath2, batch, options = {}) {
|
|
3207
|
+
const { concurrency = DEFAULT_CONCURRENCY, onProgress, onError } = options;
|
|
3208
|
+
const startTime = Date.now();
|
|
3209
|
+
const results = [];
|
|
3210
|
+
let successful = 0;
|
|
3211
|
+
let failed = 0;
|
|
3212
|
+
let processed = 0;
|
|
3213
|
+
const events = batch.events;
|
|
3214
|
+
const total = events.length;
|
|
3215
|
+
if (total === 0) {
|
|
3216
|
+
return {
|
|
3217
|
+
total: 0,
|
|
3218
|
+
successful: 0,
|
|
3219
|
+
failed: 0,
|
|
3220
|
+
results: [],
|
|
3221
|
+
durationMs: 0
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
console.error(`[flywheel] Processing ${total} file events`);
|
|
3225
|
+
for (let i = 0; i < events.length; i += concurrency) {
|
|
3226
|
+
const chunk = events.slice(i, i + concurrency);
|
|
3227
|
+
const chunkResults = await Promise.allSettled(
|
|
3228
|
+
chunk.map(async (event) => {
|
|
3229
|
+
const relativePath = getRelativePath(vaultPath2, event.path);
|
|
3230
|
+
if (event.type === "delete") {
|
|
3231
|
+
return deleteNote(index, relativePath);
|
|
3232
|
+
} else {
|
|
3233
|
+
return upsertNote(index, vaultPath2, relativePath);
|
|
3234
|
+
}
|
|
3235
|
+
})
|
|
3236
|
+
);
|
|
3237
|
+
for (let j = 0; j < chunkResults.length; j++) {
|
|
3238
|
+
const result = chunkResults[j];
|
|
3239
|
+
processed++;
|
|
3240
|
+
if (result.status === "fulfilled") {
|
|
3241
|
+
results.push(result.value);
|
|
3242
|
+
if (result.value.success) {
|
|
3243
|
+
successful++;
|
|
3244
|
+
} else {
|
|
3245
|
+
failed++;
|
|
3246
|
+
if (result.value.error && onError) {
|
|
3247
|
+
onError(result.value.path, result.value.error);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
} else {
|
|
3251
|
+
failed++;
|
|
3252
|
+
const event = chunk[j];
|
|
3253
|
+
const relativePath = getRelativePath(vaultPath2, event.path);
|
|
3254
|
+
const error = result.reason instanceof Error ? result.reason : new Error(String(result.reason));
|
|
3255
|
+
results.push({
|
|
3256
|
+
success: false,
|
|
3257
|
+
action: "unchanged",
|
|
3258
|
+
path: relativePath,
|
|
3259
|
+
error
|
|
3260
|
+
});
|
|
3261
|
+
onError?.(relativePath, error);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
onProgress?.(processed, total);
|
|
3265
|
+
if (processed % YIELD_INTERVAL === 0 && processed < total) {
|
|
3266
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
const durationMs = Date.now() - startTime;
|
|
3270
|
+
console.error(`[flywheel] Processed ${successful}/${total} files in ${durationMs}ms`);
|
|
3271
|
+
return {
|
|
3272
|
+
total,
|
|
3273
|
+
successful,
|
|
3274
|
+
failed,
|
|
3275
|
+
results,
|
|
3276
|
+
durationMs
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3026
3280
|
// src/core/read/watch/index.ts
|
|
3027
3281
|
function createVaultWatcher(options) {
|
|
3028
3282
|
const { vaultPath: vaultPath2, onBatch, onStateChange, onError } = options;
|
|
@@ -3097,31 +3351,31 @@ function createVaultWatcher(options) {
|
|
|
3097
3351
|
usePolling: config.usePolling,
|
|
3098
3352
|
interval: config.usePolling ? config.pollInterval : void 0
|
|
3099
3353
|
});
|
|
3100
|
-
watcher.on("add", (
|
|
3101
|
-
console.error(`[flywheel] RAW EVENT: add ${
|
|
3102
|
-
if (shouldWatch(
|
|
3103
|
-
console.error(`[flywheel] ACCEPTED: add ${
|
|
3104
|
-
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);
|
|
3105
3359
|
} else {
|
|
3106
|
-
console.error(`[flywheel] FILTERED: add ${
|
|
3360
|
+
console.error(`[flywheel] FILTERED: add ${path32}`);
|
|
3107
3361
|
}
|
|
3108
3362
|
});
|
|
3109
|
-
watcher.on("change", (
|
|
3110
|
-
console.error(`[flywheel] RAW EVENT: change ${
|
|
3111
|
-
if (shouldWatch(
|
|
3112
|
-
console.error(`[flywheel] ACCEPTED: change ${
|
|
3113
|
-
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);
|
|
3114
3368
|
} else {
|
|
3115
|
-
console.error(`[flywheel] FILTERED: change ${
|
|
3369
|
+
console.error(`[flywheel] FILTERED: change ${path32}`);
|
|
3116
3370
|
}
|
|
3117
3371
|
});
|
|
3118
|
-
watcher.on("unlink", (
|
|
3119
|
-
console.error(`[flywheel] RAW EVENT: unlink ${
|
|
3120
|
-
if (shouldWatch(
|
|
3121
|
-
console.error(`[flywheel] ACCEPTED: unlink ${
|
|
3122
|
-
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);
|
|
3123
3377
|
} else {
|
|
3124
|
-
console.error(`[flywheel] FILTERED: unlink ${
|
|
3378
|
+
console.error(`[flywheel] FILTERED: unlink ${path32}`);
|
|
3125
3379
|
}
|
|
3126
3380
|
});
|
|
3127
3381
|
watcher.on("ready", () => {
|
|
@@ -3325,13 +3579,13 @@ function updateSuppressionList(stateDb2) {
|
|
|
3325
3579
|
"DELETE FROM wikilink_suppressions WHERE entity = ?"
|
|
3326
3580
|
);
|
|
3327
3581
|
const transaction = stateDb2.db.transaction(() => {
|
|
3328
|
-
for (const
|
|
3329
|
-
const fpRate =
|
|
3582
|
+
for (const stat4 of stats) {
|
|
3583
|
+
const fpRate = stat4.false_positives / stat4.total;
|
|
3330
3584
|
if (fpRate >= SUPPRESSION_THRESHOLD) {
|
|
3331
|
-
upsert.run(
|
|
3585
|
+
upsert.run(stat4.entity, fpRate);
|
|
3332
3586
|
updated++;
|
|
3333
3587
|
} else {
|
|
3334
|
-
remove.run(
|
|
3588
|
+
remove.run(stat4.entity);
|
|
3335
3589
|
}
|
|
3336
3590
|
}
|
|
3337
3591
|
});
|
|
@@ -3458,6 +3712,46 @@ function getTrackedApplications(stateDb2, notePath) {
|
|
|
3458
3712
|
).all(notePath);
|
|
3459
3713
|
return rows.map((r) => r.entity);
|
|
3460
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
|
+
}
|
|
3461
3755
|
function processImplicitFeedback(stateDb2, notePath, currentContent) {
|
|
3462
3756
|
const tracked = getTrackedApplications(stateDb2, notePath);
|
|
3463
3757
|
if (tracked.length === 0) return [];
|
|
@@ -3725,7 +4019,7 @@ function getExtendedDashboardData(stateDb2) {
|
|
|
3725
4019
|
|
|
3726
4020
|
// src/core/write/git.ts
|
|
3727
4021
|
import { simpleGit, CheckRepoActions } from "simple-git";
|
|
3728
|
-
import
|
|
4022
|
+
import path8 from "path";
|
|
3729
4023
|
import fs6 from "fs/promises";
|
|
3730
4024
|
import {
|
|
3731
4025
|
setWriteState,
|
|
@@ -3779,10 +4073,10 @@ function clearLastMutationCommit() {
|
|
|
3779
4073
|
}
|
|
3780
4074
|
}
|
|
3781
4075
|
async function checkGitLock(vaultPath2) {
|
|
3782
|
-
const lockPath =
|
|
4076
|
+
const lockPath = path8.join(vaultPath2, ".git/index.lock");
|
|
3783
4077
|
try {
|
|
3784
|
-
const
|
|
3785
|
-
const ageMs = Date.now() -
|
|
4078
|
+
const stat4 = await fs6.stat(lockPath);
|
|
4079
|
+
const ageMs = Date.now() - stat4.mtimeMs;
|
|
3786
4080
|
return {
|
|
3787
4081
|
locked: true,
|
|
3788
4082
|
stale: ageMs > STALE_LOCK_THRESHOLD_MS,
|
|
@@ -3802,10 +4096,10 @@ async function isGitRepo(vaultPath2) {
|
|
|
3802
4096
|
}
|
|
3803
4097
|
}
|
|
3804
4098
|
async function checkLockFile(vaultPath2) {
|
|
3805
|
-
const lockPath =
|
|
4099
|
+
const lockPath = path8.join(vaultPath2, ".git/index.lock");
|
|
3806
4100
|
try {
|
|
3807
|
-
const
|
|
3808
|
-
const ageMs = Date.now() -
|
|
4101
|
+
const stat4 = await fs6.stat(lockPath);
|
|
4102
|
+
const ageMs = Date.now() - stat4.mtimeMs;
|
|
3809
4103
|
return { stale: ageMs > STALE_LOCK_THRESHOLD_MS, ageMs };
|
|
3810
4104
|
} catch {
|
|
3811
4105
|
return null;
|
|
@@ -3852,7 +4146,7 @@ async function commitChange(vaultPath2, filePath, messagePrefix, retryConfig = D
|
|
|
3852
4146
|
}
|
|
3853
4147
|
}
|
|
3854
4148
|
await git.add(filePath);
|
|
3855
|
-
const fileName =
|
|
4149
|
+
const fileName = path8.basename(filePath);
|
|
3856
4150
|
const commitMessage = `${messagePrefix} Update ${fileName}`;
|
|
3857
4151
|
const result = await git.commit(commitMessage);
|
|
3858
4152
|
if (result.commit) {
|
|
@@ -4046,7 +4340,7 @@ function setHintsStateDb(stateDb2) {
|
|
|
4046
4340
|
|
|
4047
4341
|
// src/core/shared/recency.ts
|
|
4048
4342
|
import { readdir, readFile, stat } from "fs/promises";
|
|
4049
|
-
import
|
|
4343
|
+
import path9 from "path";
|
|
4050
4344
|
import {
|
|
4051
4345
|
getEntityName,
|
|
4052
4346
|
recordEntityMention,
|
|
@@ -4068,9 +4362,9 @@ async function* walkMarkdownFiles(dir, baseDir) {
|
|
|
4068
4362
|
try {
|
|
4069
4363
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
4070
4364
|
for (const entry of entries) {
|
|
4071
|
-
const fullPath =
|
|
4072
|
-
const relativePath =
|
|
4073
|
-
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];
|
|
4074
4368
|
if (EXCLUDED_FOLDERS.has(topFolder)) {
|
|
4075
4369
|
continue;
|
|
4076
4370
|
}
|
|
@@ -4950,7 +5244,7 @@ function tokenize(text) {
|
|
|
4950
5244
|
|
|
4951
5245
|
// src/core/shared/cooccurrence.ts
|
|
4952
5246
|
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
4953
|
-
import
|
|
5247
|
+
import path10 from "path";
|
|
4954
5248
|
var DEFAULT_MIN_COOCCURRENCE = 2;
|
|
4955
5249
|
var EXCLUDED_FOLDERS2 = /* @__PURE__ */ new Set([
|
|
4956
5250
|
"templates",
|
|
@@ -4984,9 +5278,9 @@ async function* walkMarkdownFiles2(dir, baseDir) {
|
|
|
4984
5278
|
try {
|
|
4985
5279
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
4986
5280
|
for (const entry of entries) {
|
|
4987
|
-
const fullPath =
|
|
4988
|
-
const relativePath =
|
|
4989
|
-
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];
|
|
4990
5284
|
if (EXCLUDED_FOLDERS2.has(topFolder)) {
|
|
4991
5285
|
continue;
|
|
4992
5286
|
}
|
|
@@ -5227,7 +5521,7 @@ function sortEntitiesByPriority(entities, notePath) {
|
|
|
5227
5521
|
return priorityB - priorityA;
|
|
5228
5522
|
});
|
|
5229
5523
|
}
|
|
5230
|
-
function processWikilinks(content, notePath) {
|
|
5524
|
+
function processWikilinks(content, notePath, existingContent) {
|
|
5231
5525
|
if (!isEntityIndexReady() || !entityIndex) {
|
|
5232
5526
|
console.error("[Flywheel:DEBUG] Entity index not ready, entities:", entityIndex?._metadata?.total_entities ?? 0);
|
|
5233
5527
|
return {
|
|
@@ -5249,9 +5543,16 @@ function processWikilinks(content, notePath) {
|
|
|
5249
5543
|
const resolved = resolveAliasWikilinks(content, sortedEntities, {
|
|
5250
5544
|
caseInsensitive: true
|
|
5251
5545
|
});
|
|
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
|
+
}
|
|
5252
5552
|
const result = applyWikilinks(resolved.content, sortedEntities, {
|
|
5253
5553
|
firstOccurrenceOnly: true,
|
|
5254
|
-
caseInsensitive: true
|
|
5554
|
+
caseInsensitive: true,
|
|
5555
|
+
alreadyLinked: step1LinkedEntities
|
|
5255
5556
|
});
|
|
5256
5557
|
const implicitEnabled = moduleConfig?.implicit_detection !== false;
|
|
5257
5558
|
const validPatterns = new Set(ALL_IMPLICIT_PATTERNS);
|
|
@@ -5308,12 +5609,12 @@ function processWikilinks(content, notePath) {
|
|
|
5308
5609
|
linkedEntities: [...resolved.linkedEntities, ...result.linkedEntities]
|
|
5309
5610
|
};
|
|
5310
5611
|
}
|
|
5311
|
-
function maybeApplyWikilinks(content, skipWikilinks, notePath) {
|
|
5612
|
+
function maybeApplyWikilinks(content, skipWikilinks, notePath, existingContent) {
|
|
5312
5613
|
if (skipWikilinks) {
|
|
5313
5614
|
return { content };
|
|
5314
5615
|
}
|
|
5315
5616
|
checkAndRefreshIfStale();
|
|
5316
|
-
const result = processWikilinks(content, notePath);
|
|
5617
|
+
const result = processWikilinks(content, notePath, existingContent);
|
|
5317
5618
|
if (result.linksAdded > 0) {
|
|
5318
5619
|
if (moduleStateDb4 && notePath) {
|
|
5319
5620
|
trackWikilinkApplications(moduleStateDb4, notePath, result.linkedEntities);
|
|
@@ -6298,11 +6599,11 @@ function countFTS5Mentions(term) {
|
|
|
6298
6599
|
}
|
|
6299
6600
|
|
|
6300
6601
|
// src/core/read/taskCache.ts
|
|
6301
|
-
import * as
|
|
6602
|
+
import * as path12 from "path";
|
|
6302
6603
|
|
|
6303
6604
|
// src/tools/read/tasks.ts
|
|
6304
6605
|
import * as fs8 from "fs";
|
|
6305
|
-
import * as
|
|
6606
|
+
import * as path11 from "path";
|
|
6306
6607
|
var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
|
|
6307
6608
|
var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
|
|
6308
6609
|
var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
|
|
@@ -6371,7 +6672,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
|
|
|
6371
6672
|
const allTasks = [];
|
|
6372
6673
|
for (const note of index.notes.values()) {
|
|
6373
6674
|
if (folder && !note.path.startsWith(folder)) continue;
|
|
6374
|
-
const absolutePath =
|
|
6675
|
+
const absolutePath = path11.join(vaultPath2, note.path);
|
|
6375
6676
|
const tasks = await extractTasksFromNote(note.path, absolutePath);
|
|
6376
6677
|
allTasks.push(...tasks);
|
|
6377
6678
|
}
|
|
@@ -6415,7 +6716,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
|
|
|
6415
6716
|
async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
|
|
6416
6717
|
const note = index.notes.get(notePath);
|
|
6417
6718
|
if (!note) return null;
|
|
6418
|
-
const absolutePath =
|
|
6719
|
+
const absolutePath = path11.join(vaultPath2, notePath);
|
|
6419
6720
|
let tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6420
6721
|
if (excludeTags.length > 0) {
|
|
6421
6722
|
tasks = tasks.filter(
|
|
@@ -6507,7 +6808,7 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
6507
6808
|
}
|
|
6508
6809
|
const allRows = [];
|
|
6509
6810
|
for (const notePath of notePaths) {
|
|
6510
|
-
const absolutePath =
|
|
6811
|
+
const absolutePath = path12.join(vaultPath2, notePath);
|
|
6511
6812
|
const tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6512
6813
|
for (const task of tasks) {
|
|
6513
6814
|
if (excludeTags?.length && excludeTags.some((t) => task.tags.includes(t))) {
|
|
@@ -6549,7 +6850,7 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
6549
6850
|
async function updateTaskCacheForFile(vaultPath2, relativePath) {
|
|
6550
6851
|
if (!db3) return;
|
|
6551
6852
|
db3.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
|
|
6552
|
-
const absolutePath =
|
|
6853
|
+
const absolutePath = path12.join(vaultPath2, relativePath);
|
|
6553
6854
|
const tasks = await extractTasksFromNote(relativePath, absolutePath);
|
|
6554
6855
|
if (tasks.length > 0) {
|
|
6555
6856
|
const insertStmt = db3.prepare(`
|
|
@@ -6689,7 +6990,7 @@ import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, get
|
|
|
6689
6990
|
|
|
6690
6991
|
// src/tools/read/graph.ts
|
|
6691
6992
|
import * as fs9 from "fs";
|
|
6692
|
-
import * as
|
|
6993
|
+
import * as path13 from "path";
|
|
6693
6994
|
import { z } from "zod";
|
|
6694
6995
|
|
|
6695
6996
|
// src/core/read/constants.ts
|
|
@@ -6973,7 +7274,7 @@ function requireIndex() {
|
|
|
6973
7274
|
// src/tools/read/graph.ts
|
|
6974
7275
|
async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
|
|
6975
7276
|
try {
|
|
6976
|
-
const fullPath =
|
|
7277
|
+
const fullPath = path13.join(vaultPath2, sourcePath);
|
|
6977
7278
|
const content = await fs9.promises.readFile(fullPath, "utf-8");
|
|
6978
7279
|
const allLines = content.split("\n");
|
|
6979
7280
|
let fmLines = 0;
|
|
@@ -7287,14 +7588,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
7287
7588
|
};
|
|
7288
7589
|
function findSimilarEntity2(target, entities) {
|
|
7289
7590
|
const targetLower = target.toLowerCase();
|
|
7290
|
-
for (const [name,
|
|
7591
|
+
for (const [name, path32] of entities) {
|
|
7291
7592
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
7292
|
-
return
|
|
7593
|
+
return path32;
|
|
7293
7594
|
}
|
|
7294
7595
|
}
|
|
7295
|
-
for (const [name,
|
|
7596
|
+
for (const [name, path32] of entities) {
|
|
7296
7597
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
7297
|
-
return
|
|
7598
|
+
return path32;
|
|
7298
7599
|
}
|
|
7299
7600
|
}
|
|
7300
7601
|
return void 0;
|
|
@@ -8271,8 +8572,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8271
8572
|
daily_counts: z3.record(z3.number())
|
|
8272
8573
|
}).describe("Activity summary for the last 7 days")
|
|
8273
8574
|
};
|
|
8274
|
-
function isPeriodicNote2(
|
|
8275
|
-
const filename =
|
|
8575
|
+
function isPeriodicNote2(path32) {
|
|
8576
|
+
const filename = path32.split("/").pop() || "";
|
|
8276
8577
|
const nameWithoutExt = filename.replace(/\.md$/, "");
|
|
8277
8578
|
const patterns = [
|
|
8278
8579
|
/^\d{4}-\d{2}-\d{2}$/,
|
|
@@ -8287,7 +8588,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8287
8588
|
// YYYY (yearly)
|
|
8288
8589
|
];
|
|
8289
8590
|
const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
|
|
8290
|
-
const folder =
|
|
8591
|
+
const folder = path32.split("/")[0]?.toLowerCase() || "";
|
|
8291
8592
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
8292
8593
|
}
|
|
8293
8594
|
server2.registerTool(
|
|
@@ -8695,7 +8996,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
8695
8996
|
|
|
8696
8997
|
// src/tools/read/system.ts
|
|
8697
8998
|
import * as fs11 from "fs";
|
|
8698
|
-
import * as
|
|
8999
|
+
import * as path14 from "path";
|
|
8699
9000
|
import { z as z5 } from "zod";
|
|
8700
9001
|
import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
|
|
8701
9002
|
|
|
@@ -8995,7 +9296,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
8995
9296
|
continue;
|
|
8996
9297
|
}
|
|
8997
9298
|
try {
|
|
8998
|
-
const fullPath =
|
|
9299
|
+
const fullPath = path14.join(vaultPath2, note.path);
|
|
8999
9300
|
const content = await fs11.promises.readFile(fullPath, "utf-8");
|
|
9000
9301
|
const lines = content.split("\n");
|
|
9001
9302
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9111,7 +9412,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
9111
9412
|
let wordCount;
|
|
9112
9413
|
if (include_word_count) {
|
|
9113
9414
|
try {
|
|
9114
|
-
const fullPath =
|
|
9415
|
+
const fullPath = path14.join(vaultPath2, resolvedPath);
|
|
9115
9416
|
const content = await fs11.promises.readFile(fullPath, "utf-8");
|
|
9116
9417
|
wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
9117
9418
|
} catch {
|
|
@@ -9342,7 +9643,7 @@ import { z as z6 } from "zod";
|
|
|
9342
9643
|
|
|
9343
9644
|
// src/tools/read/structure.ts
|
|
9344
9645
|
import * as fs12 from "fs";
|
|
9345
|
-
import * as
|
|
9646
|
+
import * as path15 from "path";
|
|
9346
9647
|
var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
|
|
9347
9648
|
function extractHeadings(content) {
|
|
9348
9649
|
const lines = content.split("\n");
|
|
@@ -9396,7 +9697,7 @@ function buildSections(headings, totalLines) {
|
|
|
9396
9697
|
async function getNoteStructure(index, notePath, vaultPath2) {
|
|
9397
9698
|
const note = index.notes.get(notePath);
|
|
9398
9699
|
if (!note) return null;
|
|
9399
|
-
const absolutePath =
|
|
9700
|
+
const absolutePath = path15.join(vaultPath2, notePath);
|
|
9400
9701
|
let content;
|
|
9401
9702
|
try {
|
|
9402
9703
|
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
@@ -9419,7 +9720,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
|
|
|
9419
9720
|
async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
|
|
9420
9721
|
const note = index.notes.get(notePath);
|
|
9421
9722
|
if (!note) return null;
|
|
9422
|
-
const absolutePath =
|
|
9723
|
+
const absolutePath = path15.join(vaultPath2, notePath);
|
|
9423
9724
|
let content;
|
|
9424
9725
|
try {
|
|
9425
9726
|
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
@@ -9461,7 +9762,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
9461
9762
|
const results = [];
|
|
9462
9763
|
for (const note of index.notes.values()) {
|
|
9463
9764
|
if (folder && !note.path.startsWith(folder)) continue;
|
|
9464
|
-
const absolutePath =
|
|
9765
|
+
const absolutePath = path15.join(vaultPath2, note.path);
|
|
9465
9766
|
let content;
|
|
9466
9767
|
try {
|
|
9467
9768
|
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
@@ -9495,18 +9796,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9495
9796
|
include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
|
|
9496
9797
|
}
|
|
9497
9798
|
},
|
|
9498
|
-
async ({ path:
|
|
9799
|
+
async ({ path: path32, include_content }) => {
|
|
9499
9800
|
const index = getIndex();
|
|
9500
9801
|
const vaultPath2 = getVaultPath();
|
|
9501
|
-
const result = await getNoteStructure(index,
|
|
9802
|
+
const result = await getNoteStructure(index, path32, vaultPath2);
|
|
9502
9803
|
if (!result) {
|
|
9503
9804
|
return {
|
|
9504
|
-
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) }]
|
|
9505
9806
|
};
|
|
9506
9807
|
}
|
|
9507
9808
|
if (include_content) {
|
|
9508
9809
|
for (const section of result.sections) {
|
|
9509
|
-
const sectionResult = await getSectionContent(index,
|
|
9810
|
+
const sectionResult = await getSectionContent(index, path32, section.heading.text, vaultPath2, true);
|
|
9510
9811
|
if (sectionResult) {
|
|
9511
9812
|
section.content = sectionResult.content;
|
|
9512
9813
|
}
|
|
@@ -9528,15 +9829,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9528
9829
|
include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
|
|
9529
9830
|
}
|
|
9530
9831
|
},
|
|
9531
|
-
async ({ path:
|
|
9832
|
+
async ({ path: path32, heading, include_subheadings }) => {
|
|
9532
9833
|
const index = getIndex();
|
|
9533
9834
|
const vaultPath2 = getVaultPath();
|
|
9534
|
-
const result = await getSectionContent(index,
|
|
9835
|
+
const result = await getSectionContent(index, path32, heading, vaultPath2, include_subheadings);
|
|
9535
9836
|
if (!result) {
|
|
9536
9837
|
return {
|
|
9537
9838
|
content: [{ type: "text", text: JSON.stringify({
|
|
9538
9839
|
error: "Section not found",
|
|
9539
|
-
path:
|
|
9840
|
+
path: path32,
|
|
9540
9841
|
heading
|
|
9541
9842
|
}, null, 2) }]
|
|
9542
9843
|
};
|
|
@@ -9590,16 +9891,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9590
9891
|
offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
9591
9892
|
}
|
|
9592
9893
|
},
|
|
9593
|
-
async ({ path:
|
|
9894
|
+
async ({ path: path32, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
|
|
9594
9895
|
const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
|
|
9595
9896
|
const index = getIndex();
|
|
9596
9897
|
const vaultPath2 = getVaultPath();
|
|
9597
9898
|
const config = getConfig();
|
|
9598
|
-
if (
|
|
9599
|
-
const result2 = await getTasksFromNote(index,
|
|
9899
|
+
if (path32) {
|
|
9900
|
+
const result2 = await getTasksFromNote(index, path32, vaultPath2, config.exclude_task_tags || []);
|
|
9600
9901
|
if (!result2) {
|
|
9601
9902
|
return {
|
|
9602
|
-
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) }]
|
|
9603
9904
|
};
|
|
9604
9905
|
}
|
|
9605
9906
|
let filtered = result2;
|
|
@@ -9609,7 +9910,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9609
9910
|
const paged2 = filtered.slice(offset, offset + limit);
|
|
9610
9911
|
return {
|
|
9611
9912
|
content: [{ type: "text", text: JSON.stringify({
|
|
9612
|
-
path:
|
|
9913
|
+
path: path32,
|
|
9613
9914
|
total_count: filtered.length,
|
|
9614
9915
|
returned_count: paged2.length,
|
|
9615
9916
|
open: result2.filter((t) => t.status === "open").length,
|
|
@@ -9765,7 +10066,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9765
10066
|
// src/tools/read/migrations.ts
|
|
9766
10067
|
import { z as z7 } from "zod";
|
|
9767
10068
|
import * as fs13 from "fs/promises";
|
|
9768
|
-
import * as
|
|
10069
|
+
import * as path16 from "path";
|
|
9769
10070
|
import matter2 from "gray-matter";
|
|
9770
10071
|
function getNotesInFolder(index, folder) {
|
|
9771
10072
|
const notes = [];
|
|
@@ -9778,7 +10079,7 @@ function getNotesInFolder(index, folder) {
|
|
|
9778
10079
|
return notes;
|
|
9779
10080
|
}
|
|
9780
10081
|
async function readFileContent(notePath, vaultPath2) {
|
|
9781
|
-
const fullPath =
|
|
10082
|
+
const fullPath = path16.join(vaultPath2, notePath);
|
|
9782
10083
|
try {
|
|
9783
10084
|
return await fs13.readFile(fullPath, "utf-8");
|
|
9784
10085
|
} catch {
|
|
@@ -9786,7 +10087,7 @@ async function readFileContent(notePath, vaultPath2) {
|
|
|
9786
10087
|
}
|
|
9787
10088
|
}
|
|
9788
10089
|
async function writeFileContent(notePath, vaultPath2, content) {
|
|
9789
|
-
const fullPath =
|
|
10090
|
+
const fullPath = path16.join(vaultPath2, notePath);
|
|
9790
10091
|
try {
|
|
9791
10092
|
await fs13.writeFile(fullPath, content, "utf-8");
|
|
9792
10093
|
return true;
|
|
@@ -9967,7 +10268,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
|
|
|
9967
10268
|
|
|
9968
10269
|
// src/tools/read/graphAnalysis.ts
|
|
9969
10270
|
import fs14 from "node:fs";
|
|
9970
|
-
import
|
|
10271
|
+
import path17 from "node:path";
|
|
9971
10272
|
import { z as z8 } from "zod";
|
|
9972
10273
|
|
|
9973
10274
|
// src/tools/read/schema.ts
|
|
@@ -10729,7 +11030,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
10729
11030
|
const scored = allNotes.map((note) => {
|
|
10730
11031
|
let wordCount = 0;
|
|
10731
11032
|
try {
|
|
10732
|
-
const content = fs14.readFileSync(
|
|
11033
|
+
const content = fs14.readFileSync(path17.join(vaultPath2, note.path), "utf-8");
|
|
10733
11034
|
const body = content.replace(/^---[\s\S]*?---\n?/, "");
|
|
10734
11035
|
wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
|
|
10735
11036
|
} catch {
|
|
@@ -11309,12 +11610,12 @@ import { z as z10 } from "zod";
|
|
|
11309
11610
|
|
|
11310
11611
|
// src/tools/read/bidirectional.ts
|
|
11311
11612
|
import * as fs15 from "fs/promises";
|
|
11312
|
-
import * as
|
|
11613
|
+
import * as path18 from "path";
|
|
11313
11614
|
import matter3 from "gray-matter";
|
|
11314
11615
|
var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
|
|
11315
11616
|
var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
|
|
11316
11617
|
async function readFileContent2(notePath, vaultPath2) {
|
|
11317
|
-
const fullPath =
|
|
11618
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
11318
11619
|
try {
|
|
11319
11620
|
return await fs15.readFile(fullPath, "utf-8");
|
|
11320
11621
|
} catch {
|
|
@@ -11493,10 +11794,10 @@ async function suggestWikilinksInFrontmatter(index, notePath, vaultPath2) {
|
|
|
11493
11794
|
|
|
11494
11795
|
// src/tools/read/computed.ts
|
|
11495
11796
|
import * as fs16 from "fs/promises";
|
|
11496
|
-
import * as
|
|
11797
|
+
import * as path19 from "path";
|
|
11497
11798
|
import matter4 from "gray-matter";
|
|
11498
11799
|
async function readFileContent3(notePath, vaultPath2) {
|
|
11499
|
-
const fullPath =
|
|
11800
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
11500
11801
|
try {
|
|
11501
11802
|
return await fs16.readFile(fullPath, "utf-8");
|
|
11502
11803
|
} catch {
|
|
@@ -11504,7 +11805,7 @@ async function readFileContent3(notePath, vaultPath2) {
|
|
|
11504
11805
|
}
|
|
11505
11806
|
}
|
|
11506
11807
|
async function getFileStats(notePath, vaultPath2) {
|
|
11507
|
-
const fullPath =
|
|
11808
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
11508
11809
|
try {
|
|
11509
11810
|
const stats = await fs16.stat(fullPath);
|
|
11510
11811
|
return {
|
|
@@ -11775,7 +12076,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath, getConfi
|
|
|
11775
12076
|
init_writer();
|
|
11776
12077
|
import { z as z11 } from "zod";
|
|
11777
12078
|
import fs20 from "fs/promises";
|
|
11778
|
-
import
|
|
12079
|
+
import path22 from "path";
|
|
11779
12080
|
|
|
11780
12081
|
// src/core/write/validator.ts
|
|
11781
12082
|
var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
|
|
@@ -11978,7 +12279,7 @@ function runValidationPipeline(content, format, options = {}) {
|
|
|
11978
12279
|
// src/core/write/mutation-helpers.ts
|
|
11979
12280
|
init_writer();
|
|
11980
12281
|
import fs19 from "fs/promises";
|
|
11981
|
-
import
|
|
12282
|
+
import path21 from "path";
|
|
11982
12283
|
init_constants();
|
|
11983
12284
|
init_writer();
|
|
11984
12285
|
function formatMcpResult(result) {
|
|
@@ -12027,7 +12328,7 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
|
|
|
12027
12328
|
return info;
|
|
12028
12329
|
}
|
|
12029
12330
|
async function ensureFileExists(vaultPath2, notePath) {
|
|
12030
|
-
const fullPath =
|
|
12331
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
12031
12332
|
try {
|
|
12032
12333
|
await fs19.access(fullPath);
|
|
12033
12334
|
return null;
|
|
@@ -12086,7 +12387,7 @@ async function withVaultFile(options, operation) {
|
|
|
12086
12387
|
if ("error" in result) {
|
|
12087
12388
|
return formatMcpResult(result.error);
|
|
12088
12389
|
}
|
|
12089
|
-
const fullPath =
|
|
12390
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
12090
12391
|
const statBefore = await fs19.stat(fullPath);
|
|
12091
12392
|
if (statBefore.mtimeMs !== result.mtimeMs) {
|
|
12092
12393
|
console.warn(`[withVaultFile] External modification detected on ${notePath}, re-reading and retrying`);
|
|
@@ -12149,10 +12450,10 @@ async function withVaultFrontmatter(options, operation) {
|
|
|
12149
12450
|
|
|
12150
12451
|
// src/tools/write/mutations.ts
|
|
12151
12452
|
async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
12152
|
-
const fullPath =
|
|
12153
|
-
await fs20.mkdir(
|
|
12453
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
12454
|
+
await fs20.mkdir(path22.dirname(fullPath), { recursive: true });
|
|
12154
12455
|
const templates = config.templates || {};
|
|
12155
|
-
const filename =
|
|
12456
|
+
const filename = path22.basename(notePath, ".md").toLowerCase();
|
|
12156
12457
|
let templatePath;
|
|
12157
12458
|
const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
|
|
12158
12459
|
const weeklyPattern = /^\d{4}-W\d{2}/;
|
|
@@ -12173,10 +12474,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
12173
12474
|
let templateContent;
|
|
12174
12475
|
if (templatePath) {
|
|
12175
12476
|
try {
|
|
12176
|
-
const absTemplatePath =
|
|
12477
|
+
const absTemplatePath = path22.join(vaultPath2, templatePath);
|
|
12177
12478
|
templateContent = await fs20.readFile(absTemplatePath, "utf-8");
|
|
12178
12479
|
} catch {
|
|
12179
|
-
const title =
|
|
12480
|
+
const title = path22.basename(notePath, ".md");
|
|
12180
12481
|
templateContent = `---
|
|
12181
12482
|
---
|
|
12182
12483
|
|
|
@@ -12185,7 +12486,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
12185
12486
|
templatePath = void 0;
|
|
12186
12487
|
}
|
|
12187
12488
|
} else {
|
|
12188
|
-
const title =
|
|
12489
|
+
const title = path22.basename(notePath, ".md");
|
|
12189
12490
|
templateContent = `---
|
|
12190
12491
|
---
|
|
12191
12492
|
|
|
@@ -12194,7 +12495,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
12194
12495
|
}
|
|
12195
12496
|
const now = /* @__PURE__ */ new Date();
|
|
12196
12497
|
const dateStr = now.toISOString().split("T")[0];
|
|
12197
|
-
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g,
|
|
12498
|
+
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path22.basename(notePath, ".md"));
|
|
12198
12499
|
const matter9 = (await import("gray-matter")).default;
|
|
12199
12500
|
const parsed = matter9(templateContent);
|
|
12200
12501
|
if (!parsed.data.date) {
|
|
@@ -12234,7 +12535,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
12234
12535
|
let noteCreated = false;
|
|
12235
12536
|
let templateUsed;
|
|
12236
12537
|
if (create_if_missing) {
|
|
12237
|
-
const fullPath =
|
|
12538
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
12238
12539
|
try {
|
|
12239
12540
|
await fs20.access(fullPath);
|
|
12240
12541
|
} catch {
|
|
@@ -12264,7 +12565,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
12264
12565
|
throw new Error(validationResult.blockReason || "Output validation failed");
|
|
12265
12566
|
}
|
|
12266
12567
|
let workingContent = validationResult.content;
|
|
12267
|
-
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(workingContent, skipWikilinks, notePath);
|
|
12568
|
+
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(workingContent, skipWikilinks, notePath, ctx.content);
|
|
12268
12569
|
if (linkedEntities?.length) {
|
|
12269
12570
|
const stateDb2 = getWriteStateDb();
|
|
12270
12571
|
if (stateDb2) {
|
|
@@ -12693,7 +12994,7 @@ Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { st
|
|
|
12693
12994
|
init_writer();
|
|
12694
12995
|
import { z as z14 } from "zod";
|
|
12695
12996
|
import fs21 from "fs/promises";
|
|
12696
|
-
import
|
|
12997
|
+
import path23 from "path";
|
|
12697
12998
|
function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
12698
12999
|
server2.tool(
|
|
12699
13000
|
"vault_create_note",
|
|
@@ -12716,23 +13017,23 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
12716
13017
|
if (!validatePath(vaultPath2, notePath)) {
|
|
12717
13018
|
return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
|
|
12718
13019
|
}
|
|
12719
|
-
const fullPath =
|
|
13020
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
12720
13021
|
const existsCheck = await ensureFileExists(vaultPath2, notePath);
|
|
12721
13022
|
if (existsCheck === null && !overwrite) {
|
|
12722
13023
|
return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
|
|
12723
13024
|
}
|
|
12724
|
-
const dir =
|
|
13025
|
+
const dir = path23.dirname(fullPath);
|
|
12725
13026
|
await fs21.mkdir(dir, { recursive: true });
|
|
12726
13027
|
let effectiveContent = content;
|
|
12727
13028
|
let effectiveFrontmatter = frontmatter;
|
|
12728
13029
|
if (template) {
|
|
12729
|
-
const templatePath =
|
|
13030
|
+
const templatePath = path23.join(vaultPath2, template);
|
|
12730
13031
|
try {
|
|
12731
13032
|
const raw = await fs21.readFile(templatePath, "utf-8");
|
|
12732
13033
|
const matter9 = (await import("gray-matter")).default;
|
|
12733
13034
|
const parsed = matter9(raw);
|
|
12734
13035
|
const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
12735
|
-
const title =
|
|
13036
|
+
const title = path23.basename(notePath, ".md");
|
|
12736
13037
|
let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
|
|
12737
13038
|
if (content) {
|
|
12738
13039
|
templateContent = templateContent.trimEnd() + "\n\n" + content;
|
|
@@ -12751,7 +13052,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
12751
13052
|
effectiveFrontmatter.created = now.toISOString();
|
|
12752
13053
|
}
|
|
12753
13054
|
const warnings = [];
|
|
12754
|
-
const noteName =
|
|
13055
|
+
const noteName = path23.basename(notePath, ".md");
|
|
12755
13056
|
const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
|
|
12756
13057
|
const preflight = await checkPreflightSimilarity(noteName);
|
|
12757
13058
|
if (preflight.existingEntity) {
|
|
@@ -12868,7 +13169,7 @@ ${sources}`;
|
|
|
12868
13169
|
}
|
|
12869
13170
|
return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
|
|
12870
13171
|
}
|
|
12871
|
-
const fullPath =
|
|
13172
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
12872
13173
|
await fs21.unlink(fullPath);
|
|
12873
13174
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
|
|
12874
13175
|
const message = backlinkWarning ? `Deleted note: ${notePath}
|
|
@@ -12888,7 +13189,7 @@ Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
|
|
|
12888
13189
|
init_writer();
|
|
12889
13190
|
import { z as z15 } from "zod";
|
|
12890
13191
|
import fs22 from "fs/promises";
|
|
12891
|
-
import
|
|
13192
|
+
import path24 from "path";
|
|
12892
13193
|
import matter6 from "gray-matter";
|
|
12893
13194
|
function escapeRegex(str) {
|
|
12894
13195
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -12907,7 +13208,7 @@ function extractWikilinks2(content) {
|
|
|
12907
13208
|
return wikilinks;
|
|
12908
13209
|
}
|
|
12909
13210
|
function getTitleFromPath(filePath) {
|
|
12910
|
-
return
|
|
13211
|
+
return path24.basename(filePath, ".md");
|
|
12911
13212
|
}
|
|
12912
13213
|
async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
12913
13214
|
const results = [];
|
|
@@ -12916,7 +13217,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
12916
13217
|
const files = [];
|
|
12917
13218
|
const entries = await fs22.readdir(dir, { withFileTypes: true });
|
|
12918
13219
|
for (const entry of entries) {
|
|
12919
|
-
const fullPath =
|
|
13220
|
+
const fullPath = path24.join(dir, entry.name);
|
|
12920
13221
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
12921
13222
|
files.push(...await scanDir(fullPath));
|
|
12922
13223
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -12927,7 +13228,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
12927
13228
|
}
|
|
12928
13229
|
const allFiles = await scanDir(vaultPath2);
|
|
12929
13230
|
for (const filePath of allFiles) {
|
|
12930
|
-
const relativePath =
|
|
13231
|
+
const relativePath = path24.relative(vaultPath2, filePath);
|
|
12931
13232
|
const content = await fs22.readFile(filePath, "utf-8");
|
|
12932
13233
|
const wikilinks = extractWikilinks2(content);
|
|
12933
13234
|
const matchingLinks = [];
|
|
@@ -12947,7 +13248,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
12947
13248
|
return results;
|
|
12948
13249
|
}
|
|
12949
13250
|
async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
|
|
12950
|
-
const fullPath =
|
|
13251
|
+
const fullPath = path24.join(vaultPath2, filePath);
|
|
12951
13252
|
const raw = await fs22.readFile(fullPath, "utf-8");
|
|
12952
13253
|
const parsed = matter6(raw);
|
|
12953
13254
|
let content = parsed.content;
|
|
@@ -13014,8 +13315,8 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
13014
13315
|
};
|
|
13015
13316
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
13016
13317
|
}
|
|
13017
|
-
const oldFullPath =
|
|
13018
|
-
const newFullPath =
|
|
13318
|
+
const oldFullPath = path24.join(vaultPath2, oldPath);
|
|
13319
|
+
const newFullPath = path24.join(vaultPath2, newPath);
|
|
13019
13320
|
try {
|
|
13020
13321
|
await fs22.access(oldFullPath);
|
|
13021
13322
|
} catch {
|
|
@@ -13065,7 +13366,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
13065
13366
|
}
|
|
13066
13367
|
}
|
|
13067
13368
|
}
|
|
13068
|
-
const destDir =
|
|
13369
|
+
const destDir = path24.dirname(newFullPath);
|
|
13069
13370
|
await fs22.mkdir(destDir, { recursive: true });
|
|
13070
13371
|
await fs22.rename(oldFullPath, newFullPath);
|
|
13071
13372
|
let gitCommit;
|
|
@@ -13151,10 +13452,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
13151
13452
|
if (sanitizedTitle !== newTitle) {
|
|
13152
13453
|
console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
|
|
13153
13454
|
}
|
|
13154
|
-
const fullPath =
|
|
13155
|
-
const dir =
|
|
13156
|
-
const newPath = dir === "." ? `${sanitizedTitle}.md` :
|
|
13157
|
-
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);
|
|
13158
13459
|
try {
|
|
13159
13460
|
await fs22.access(fullPath);
|
|
13160
13461
|
} catch {
|
|
@@ -13511,7 +13812,7 @@ init_schema();
|
|
|
13511
13812
|
// src/core/write/policy/parser.ts
|
|
13512
13813
|
init_schema();
|
|
13513
13814
|
import fs24 from "fs/promises";
|
|
13514
|
-
import
|
|
13815
|
+
import path25 from "path";
|
|
13515
13816
|
import matter7 from "gray-matter";
|
|
13516
13817
|
function parseYaml(content) {
|
|
13517
13818
|
const parsed = matter7(`---
|
|
@@ -13560,13 +13861,13 @@ async function loadPolicyFile(filePath) {
|
|
|
13560
13861
|
}
|
|
13561
13862
|
}
|
|
13562
13863
|
async function loadPolicy(vaultPath2, policyName) {
|
|
13563
|
-
const policiesDir =
|
|
13564
|
-
const policyPath =
|
|
13864
|
+
const policiesDir = path25.join(vaultPath2, ".claude", "policies");
|
|
13865
|
+
const policyPath = path25.join(policiesDir, `${policyName}.yaml`);
|
|
13565
13866
|
try {
|
|
13566
13867
|
await fs24.access(policyPath);
|
|
13567
13868
|
return loadPolicyFile(policyPath);
|
|
13568
13869
|
} catch {
|
|
13569
|
-
const ymlPath =
|
|
13870
|
+
const ymlPath = path25.join(policiesDir, `${policyName}.yml`);
|
|
13570
13871
|
try {
|
|
13571
13872
|
await fs24.access(ymlPath);
|
|
13572
13873
|
return loadPolicyFile(ymlPath);
|
|
@@ -13707,7 +14008,7 @@ init_conditions();
|
|
|
13707
14008
|
init_schema();
|
|
13708
14009
|
init_writer();
|
|
13709
14010
|
import fs26 from "fs/promises";
|
|
13710
|
-
import
|
|
14011
|
+
import path27 from "path";
|
|
13711
14012
|
init_constants();
|
|
13712
14013
|
async function executeStep(step, vaultPath2, context, conditionResults) {
|
|
13713
14014
|
const { execute, reason } = shouldStepExecute(step.when, conditionResults);
|
|
@@ -13781,7 +14082,7 @@ async function executeAddToSection(params, vaultPath2, context) {
|
|
|
13781
14082
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
13782
14083
|
const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
|
|
13783
14084
|
const maxSuggestions = Number(params.maxSuggestions) || 3;
|
|
13784
|
-
const fullPath =
|
|
14085
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
13785
14086
|
try {
|
|
13786
14087
|
await fs26.access(fullPath);
|
|
13787
14088
|
} catch {
|
|
@@ -13821,7 +14122,7 @@ async function executeRemoveFromSection(params, vaultPath2) {
|
|
|
13821
14122
|
const pattern = String(params.pattern || "");
|
|
13822
14123
|
const mode = params.mode || "first";
|
|
13823
14124
|
const useRegex = Boolean(params.useRegex);
|
|
13824
|
-
const fullPath =
|
|
14125
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
13825
14126
|
try {
|
|
13826
14127
|
await fs26.access(fullPath);
|
|
13827
14128
|
} catch {
|
|
@@ -13852,7 +14153,7 @@ async function executeReplaceInSection(params, vaultPath2, context) {
|
|
|
13852
14153
|
const mode = params.mode || "first";
|
|
13853
14154
|
const useRegex = Boolean(params.useRegex);
|
|
13854
14155
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
13855
|
-
const fullPath =
|
|
14156
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
13856
14157
|
try {
|
|
13857
14158
|
await fs26.access(fullPath);
|
|
13858
14159
|
} catch {
|
|
@@ -13895,7 +14196,7 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
13895
14196
|
if (!validatePath(vaultPath2, notePath)) {
|
|
13896
14197
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
13897
14198
|
}
|
|
13898
|
-
const fullPath =
|
|
14199
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
13899
14200
|
try {
|
|
13900
14201
|
await fs26.access(fullPath);
|
|
13901
14202
|
if (!overwrite) {
|
|
@@ -13903,7 +14204,7 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
13903
14204
|
}
|
|
13904
14205
|
} catch {
|
|
13905
14206
|
}
|
|
13906
|
-
const dir =
|
|
14207
|
+
const dir = path27.dirname(fullPath);
|
|
13907
14208
|
await fs26.mkdir(dir, { recursive: true });
|
|
13908
14209
|
const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
|
|
13909
14210
|
await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
|
|
@@ -13923,7 +14224,7 @@ async function executeDeleteNote(params, vaultPath2) {
|
|
|
13923
14224
|
if (!validatePath(vaultPath2, notePath)) {
|
|
13924
14225
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
13925
14226
|
}
|
|
13926
|
-
const fullPath =
|
|
14227
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
13927
14228
|
try {
|
|
13928
14229
|
await fs26.access(fullPath);
|
|
13929
14230
|
} catch {
|
|
@@ -13940,7 +14241,7 @@ async function executeToggleTask(params, vaultPath2) {
|
|
|
13940
14241
|
const notePath = String(params.path || "");
|
|
13941
14242
|
const task = String(params.task || "");
|
|
13942
14243
|
const section = params.section ? String(params.section) : void 0;
|
|
13943
|
-
const fullPath =
|
|
14244
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
13944
14245
|
try {
|
|
13945
14246
|
await fs26.access(fullPath);
|
|
13946
14247
|
} catch {
|
|
@@ -13983,7 +14284,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
13983
14284
|
const completed = Boolean(params.completed);
|
|
13984
14285
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
13985
14286
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
13986
|
-
const fullPath =
|
|
14287
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
13987
14288
|
try {
|
|
13988
14289
|
await fs26.access(fullPath);
|
|
13989
14290
|
} catch {
|
|
@@ -14020,7 +14321,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
14020
14321
|
async function executeUpdateFrontmatter(params, vaultPath2) {
|
|
14021
14322
|
const notePath = String(params.path || "");
|
|
14022
14323
|
const updates = params.frontmatter || {};
|
|
14023
|
-
const fullPath =
|
|
14324
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14024
14325
|
try {
|
|
14025
14326
|
await fs26.access(fullPath);
|
|
14026
14327
|
} catch {
|
|
@@ -14042,7 +14343,7 @@ async function executeAddFrontmatterField(params, vaultPath2) {
|
|
|
14042
14343
|
const notePath = String(params.path || "");
|
|
14043
14344
|
const key = String(params.key || "");
|
|
14044
14345
|
const value = params.value;
|
|
14045
|
-
const fullPath =
|
|
14346
|
+
const fullPath = path27.join(vaultPath2, notePath);
|
|
14046
14347
|
try {
|
|
14047
14348
|
await fs26.access(fullPath);
|
|
14048
14349
|
} catch {
|
|
@@ -14205,7 +14506,7 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
|
|
|
14205
14506
|
async function rollbackChanges(vaultPath2, originalContents, filesModified) {
|
|
14206
14507
|
for (const filePath of filesModified) {
|
|
14207
14508
|
const original = originalContents.get(filePath);
|
|
14208
|
-
const fullPath =
|
|
14509
|
+
const fullPath = path27.join(vaultPath2, filePath);
|
|
14209
14510
|
if (original === null) {
|
|
14210
14511
|
try {
|
|
14211
14512
|
await fs26.unlink(fullPath);
|
|
@@ -14260,9 +14561,9 @@ async function previewPolicy(policy, vaultPath2, variables) {
|
|
|
14260
14561
|
|
|
14261
14562
|
// src/core/write/policy/storage.ts
|
|
14262
14563
|
import fs27 from "fs/promises";
|
|
14263
|
-
import
|
|
14564
|
+
import path28 from "path";
|
|
14264
14565
|
function getPoliciesDir(vaultPath2) {
|
|
14265
|
-
return
|
|
14566
|
+
return path28.join(vaultPath2, ".claude", "policies");
|
|
14266
14567
|
}
|
|
14267
14568
|
async function ensurePoliciesDir(vaultPath2) {
|
|
14268
14569
|
const dir = getPoliciesDir(vaultPath2);
|
|
@@ -14277,15 +14578,15 @@ async function listPolicies(vaultPath2) {
|
|
|
14277
14578
|
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
|
|
14278
14579
|
continue;
|
|
14279
14580
|
}
|
|
14280
|
-
const filePath =
|
|
14281
|
-
const
|
|
14581
|
+
const filePath = path28.join(dir, file);
|
|
14582
|
+
const stat4 = await fs27.stat(filePath);
|
|
14282
14583
|
const content = await fs27.readFile(filePath, "utf-8");
|
|
14283
14584
|
const metadata = extractPolicyMetadata(content);
|
|
14284
14585
|
policies.push({
|
|
14285
14586
|
name: metadata.name || file.replace(/\.ya?ml$/, ""),
|
|
14286
14587
|
description: metadata.description || "No description",
|
|
14287
14588
|
path: file,
|
|
14288
|
-
lastModified:
|
|
14589
|
+
lastModified: stat4.mtime,
|
|
14289
14590
|
version: metadata.version || "1.0",
|
|
14290
14591
|
requiredVariables: metadata.variables || []
|
|
14291
14592
|
});
|
|
@@ -14302,7 +14603,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
|
|
|
14302
14603
|
const dir = getPoliciesDir(vaultPath2);
|
|
14303
14604
|
await ensurePoliciesDir(vaultPath2);
|
|
14304
14605
|
const filename = `${policyName}.yaml`;
|
|
14305
|
-
const filePath =
|
|
14606
|
+
const filePath = path28.join(dir, filename);
|
|
14306
14607
|
if (!overwrite) {
|
|
14307
14608
|
try {
|
|
14308
14609
|
await fs27.access(filePath);
|
|
@@ -14846,7 +15147,7 @@ import { z as z20 } from "zod";
|
|
|
14846
15147
|
|
|
14847
15148
|
// src/core/write/tagRename.ts
|
|
14848
15149
|
import * as fs28 from "fs/promises";
|
|
14849
|
-
import * as
|
|
15150
|
+
import * as path29 from "path";
|
|
14850
15151
|
import matter8 from "gray-matter";
|
|
14851
15152
|
import { getProtectedZones } from "@velvetmonkey/vault-core";
|
|
14852
15153
|
function getNotesInFolder3(index, folder) {
|
|
@@ -14952,7 +15253,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
|
|
|
14952
15253
|
const previews = [];
|
|
14953
15254
|
let totalChanges = 0;
|
|
14954
15255
|
for (const note of affectedNotes) {
|
|
14955
|
-
const fullPath =
|
|
15256
|
+
const fullPath = path29.join(vaultPath2, note.path);
|
|
14956
15257
|
let fileContent;
|
|
14957
15258
|
try {
|
|
14958
15259
|
fileContent = await fs28.readFile(fullPath, "utf-8");
|
|
@@ -15590,8 +15891,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
|
15590
15891
|
}
|
|
15591
15892
|
}
|
|
15592
15893
|
}
|
|
15593
|
-
return Array.from(noteMap.entries()).map(([
|
|
15594
|
-
path:
|
|
15894
|
+
return Array.from(noteMap.entries()).map(([path32, stats]) => ({
|
|
15895
|
+
path: path32,
|
|
15595
15896
|
access_count: stats.access_count,
|
|
15596
15897
|
last_accessed: stats.last_accessed,
|
|
15597
15898
|
tools_used: Array.from(stats.tools)
|
|
@@ -15744,7 +16045,7 @@ import { z as z25 } from "zod";
|
|
|
15744
16045
|
|
|
15745
16046
|
// src/core/read/similarity.ts
|
|
15746
16047
|
import * as fs29 from "fs";
|
|
15747
|
-
import * as
|
|
16048
|
+
import * as path30 from "path";
|
|
15748
16049
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
15749
16050
|
"the",
|
|
15750
16051
|
"be",
|
|
@@ -15881,7 +16182,7 @@ function extractKeyTerms(content, maxTerms = 15) {
|
|
|
15881
16182
|
}
|
|
15882
16183
|
function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
15883
16184
|
const limit = options.limit ?? 10;
|
|
15884
|
-
const absPath =
|
|
16185
|
+
const absPath = path30.join(vaultPath2, sourcePath);
|
|
15885
16186
|
let content;
|
|
15886
16187
|
try {
|
|
15887
16188
|
content = fs29.readFileSync(absPath, "utf-8");
|
|
@@ -16009,7 +16310,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16009
16310
|
exclude_linked: z25.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
|
|
16010
16311
|
}
|
|
16011
16312
|
},
|
|
16012
|
-
async ({ path:
|
|
16313
|
+
async ({ path: path32, limit, exclude_linked }) => {
|
|
16013
16314
|
const index = getIndex();
|
|
16014
16315
|
const vaultPath2 = getVaultPath();
|
|
16015
16316
|
const stateDb2 = getStateDb();
|
|
@@ -16018,10 +16319,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16018
16319
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
16019
16320
|
};
|
|
16020
16321
|
}
|
|
16021
|
-
if (!index.notes.has(
|
|
16322
|
+
if (!index.notes.has(path32)) {
|
|
16022
16323
|
return {
|
|
16023
16324
|
content: [{ type: "text", text: JSON.stringify({
|
|
16024
|
-
error: `Note not found: ${
|
|
16325
|
+
error: `Note not found: ${path32}`,
|
|
16025
16326
|
hint: "Use the full relative path including .md extension"
|
|
16026
16327
|
}, null, 2) }]
|
|
16027
16328
|
};
|
|
@@ -16032,12 +16333,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
16032
16333
|
};
|
|
16033
16334
|
const useHybrid = hasEmbeddingsIndex();
|
|
16034
16335
|
const method = useHybrid ? "hybrid" : "bm25";
|
|
16035
|
-
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);
|
|
16036
16337
|
return {
|
|
16037
16338
|
content: [{
|
|
16038
16339
|
type: "text",
|
|
16039
16340
|
text: JSON.stringify({
|
|
16040
|
-
source:
|
|
16341
|
+
source: path32,
|
|
16041
16342
|
method,
|
|
16042
16343
|
exclude_linked: exclude_linked ?? true,
|
|
16043
16344
|
count: results.length,
|
|
@@ -16275,6 +16576,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
16275
16576
|
|
|
16276
16577
|
// src/index.ts
|
|
16277
16578
|
import * as fs30 from "node:fs/promises";
|
|
16579
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
16278
16580
|
|
|
16279
16581
|
// src/resources/vault.ts
|
|
16280
16582
|
function registerVaultResources(server2, getIndex) {
|
|
@@ -16795,6 +17097,38 @@ async function updateEntitiesInStateDb() {
|
|
|
16795
17097
|
serverLog("index", `Failed to update entities in StateDb: ${e instanceof Error ? e.message : e}`, "error");
|
|
16796
17098
|
}
|
|
16797
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
|
+
}
|
|
16798
17132
|
async function runPostIndexWork(index) {
|
|
16799
17133
|
const postStart = Date.now();
|
|
16800
17134
|
serverLog("index", "Scanning entities...");
|
|
@@ -16885,277 +17219,502 @@ async function runPostIndexWork(index) {
|
|
|
16885
17219
|
}
|
|
16886
17220
|
if (process.env.FLYWHEEL_WATCH !== "false") {
|
|
16887
17221
|
const config = parseWatcherConfig();
|
|
17222
|
+
const lastContentHashes = /* @__PURE__ */ new Map();
|
|
16888
17223
|
serverLog("watcher", `File watcher enabled (debounce: ${config.debounceMs}ms)`);
|
|
16889
|
-
const
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16893
|
-
|
|
16894
|
-
|
|
16895
|
-
|
|
16896
|
-
|
|
16897
|
-
|
|
16898
|
-
|
|
16899
|
-
|
|
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, "/");
|
|
16900
17238
|
for (const prefix of vaultPrefixes) {
|
|
16901
|
-
if (
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
17239
|
+
if (resolved.startsWith(prefix + "/")) {
|
|
17240
|
+
return resolved.slice(prefix.length + 1);
|
|
17241
|
+
}
|
|
17242
|
+
}
|
|
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;
|
|
17252
|
+
}
|
|
17253
|
+
}
|
|
17254
|
+
} catch {
|
|
17255
|
+
}
|
|
17256
|
+
}
|
|
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}`);
|
|
17279
|
+
continue;
|
|
17280
|
+
}
|
|
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);
|
|
17317
|
+
}
|
|
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");
|
|
17322
|
+
}
|
|
17323
|
+
}
|
|
17324
|
+
if (filteredEvents.length === 0) {
|
|
17325
|
+
if (batchRenames.length > 0) {
|
|
17326
|
+
serverLog("watcher", `Batch complete (renames only): ${batchRenames.length} rename(s)`);
|
|
17327
|
+
} else {
|
|
17328
|
+
serverLog("watcher", "All files unchanged (hash gate), skipping batch");
|
|
17329
|
+
}
|
|
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 });
|
|
16905
17382
|
}
|
|
16906
17383
|
}
|
|
16907
|
-
|
|
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 });
|
|
17400
|
+
try {
|
|
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`);
|
|
17409
|
+
} else {
|
|
17410
|
+
tracker.end({ rebuilt: false, cached_age_ms: cacheAgeMs });
|
|
17411
|
+
serverLog("watcher", `Recency: cache valid (${Math.round(cacheAgeMs / 1e3)}s old)`);
|
|
17412
|
+
}
|
|
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) {
|
|
16908
17422
|
try {
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
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++;
|
|
16916
17430
|
}
|
|
16917
17431
|
} catch {
|
|
16918
|
-
|
|
16919
|
-
|
|
16920
|
-
|
|
16921
|
-
|
|
16922
|
-
|
|
16923
|
-
|
|
16924
|
-
|
|
16925
|
-
|
|
16926
|
-
|
|
16927
|
-
|
|
16928
|
-
|
|
16929
|
-
|
|
16930
|
-
|
|
17432
|
+
}
|
|
17433
|
+
}
|
|
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);
|
|
16931
17457
|
}
|
|
16932
17458
|
}
|
|
17459
|
+
} catch {
|
|
16933
17460
|
}
|
|
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");
|
|
16934
17465
|
}
|
|
16935
|
-
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
|
|
16939
|
-
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
|
|
16943
|
-
|
|
16944
|
-
serverLog("watcher", `Index rebuilt: ${vaultIndex.notes.size} notes, ${vaultIndex.entities.size} entities`);
|
|
16945
|
-
const hubBefore = /* @__PURE__ */ new Map();
|
|
16946
|
-
if (stateDb) {
|
|
16947
|
-
const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
|
|
16948
|
-
for (const r of rows) hubBefore.set(r.name, r.hub_score);
|
|
17466
|
+
if (stateDb) {
|
|
17467
|
+
tracker.start("index_cache", { note_count: vaultIndex.notes.size });
|
|
17468
|
+
try {
|
|
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");
|
|
16949
17475
|
}
|
|
16950
|
-
|
|
16951
|
-
tracker.
|
|
16952
|
-
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
16956
|
-
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
|
|
16960
|
-
|
|
16961
|
-
|
|
16962
|
-
|
|
16963
|
-
|
|
16964
|
-
if (prev !== r.hub_score) hubDiffs.push({ entity: r.name, before: prev, after: r.hub_score });
|
|
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++;
|
|
16965
17490
|
}
|
|
17491
|
+
} catch {
|
|
16966
17492
|
}
|
|
16967
|
-
|
|
16968
|
-
|
|
16969
|
-
|
|
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;
|
|
16970
17503
|
try {
|
|
16971
|
-
const
|
|
16972
|
-
const
|
|
16973
|
-
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
16978
|
-
|
|
16979
|
-
|
|
16980
|
-
|
|
16981
|
-
serverLog("watcher", `Recency: cache valid (${Math.round(cacheAgeMs / 1e3)}s old)`);
|
|
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);
|
|
16982
17514
|
}
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
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 {
|
|
16986
17521
|
}
|
|
16987
|
-
|
|
16988
|
-
|
|
16989
|
-
|
|
16990
|
-
|
|
16991
|
-
|
|
16992
|
-
|
|
16993
|
-
|
|
16994
|
-
|
|
16995
|
-
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
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);
|
|
17000
17562
|
}
|
|
17001
|
-
} catch {
|
|
17002
17563
|
}
|
|
17003
17564
|
}
|
|
17004
|
-
tracker.end({ updated: embUpdated, removed: embRemoved });
|
|
17005
|
-
serverLog("watcher", `Note embeddings: ${embUpdated} updated, ${embRemoved} removed`);
|
|
17006
|
-
} else {
|
|
17007
|
-
tracker.skip("note_embeddings", "not built");
|
|
17008
17565
|
}
|
|
17009
|
-
|
|
17010
|
-
|
|
17011
|
-
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
for (const event of batch.events) {
|
|
17016
|
-
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
17017
|
-
const matching = allEntities.filter((e) => e.path === event.path);
|
|
17018
|
-
for (const entity of matching) {
|
|
17019
|
-
await updateEntityEmbedding(entity.name, {
|
|
17020
|
-
name: entity.name,
|
|
17021
|
-
path: entity.path,
|
|
17022
|
-
category: entity.category,
|
|
17023
|
-
aliases: entity.aliases
|
|
17024
|
-
}, vaultPath);
|
|
17025
|
-
entEmbUpdated++;
|
|
17026
|
-
entEmbNames.push(entity.name);
|
|
17027
|
-
}
|
|
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());
|
|
17028
17572
|
}
|
|
17029
|
-
} catch {
|
|
17030
17573
|
}
|
|
17031
|
-
tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10) });
|
|
17032
|
-
serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated`);
|
|
17033
|
-
} else {
|
|
17034
|
-
tracker.skip("entity_embeddings", !stateDb ? "no stateDb" : "not built");
|
|
17035
17574
|
}
|
|
17036
|
-
|
|
17037
|
-
|
|
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) {
|
|
17586
|
+
for (const event of filteredEvents) {
|
|
17587
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
17038
17588
|
try {
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
} catch (err) {
|
|
17043
|
-
tracker.end({ saved: false, error: err instanceof Error ? err.message : String(err) });
|
|
17044
|
-
serverLog("index", `Failed to update index cache: ${err instanceof Error ? err.message : err}`, "error");
|
|
17589
|
+
const apps = getTrackedApplications(stateDb, event.path);
|
|
17590
|
+
if (apps.length > 0) trackedLinks.push({ file: event.path, entities: apps });
|
|
17591
|
+
} catch {
|
|
17045
17592
|
}
|
|
17046
|
-
} else {
|
|
17047
|
-
tracker.skip("index_cache", "no stateDb");
|
|
17048
17593
|
}
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
|
|
17054
|
-
|
|
17055
|
-
|
|
17056
|
-
|
|
17057
|
-
|
|
17058
|
-
|
|
17059
|
-
taskUpdated++;
|
|
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);
|
|
17060
17604
|
}
|
|
17061
|
-
} catch {
|
|
17062
17605
|
}
|
|
17606
|
+
} else {
|
|
17607
|
+
trackedLinks.push({ file: diff.file, entities: diff.added });
|
|
17063
17608
|
}
|
|
17064
|
-
|
|
17065
|
-
|
|
17066
|
-
|
|
17067
|
-
|
|
17068
|
-
|
|
17069
|
-
|
|
17070
|
-
for (const event of
|
|
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) {
|
|
17615
|
+
for (const event of filteredEvents) {
|
|
17071
17616
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
17072
17617
|
try {
|
|
17073
|
-
const
|
|
17074
|
-
const
|
|
17075
|
-
const
|
|
17076
|
-
const seen = /* @__PURE__ */ new Set();
|
|
17077
|
-
for (const link of links) {
|
|
17078
|
-
const name = link.target;
|
|
17079
|
-
if (seen.has(name.toLowerCase())) continue;
|
|
17080
|
-
seen.add(name.toLowerCase());
|
|
17081
|
-
if (link.exists) resolved.push(name);
|
|
17082
|
-
else dead.push(name);
|
|
17083
|
-
}
|
|
17084
|
-
if (resolved.length > 0 || dead.length > 0) {
|
|
17085
|
-
forwardLinkResults.push({ file: event.path, resolved, dead });
|
|
17086
|
-
}
|
|
17087
|
-
totalResolved += resolved.length;
|
|
17088
|
-
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 });
|
|
17089
17621
|
} catch {
|
|
17090
17622
|
}
|
|
17091
17623
|
}
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17100
|
-
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
try {
|
|
17104
|
-
const apps = getTrackedApplications(stateDb, event.path);
|
|
17105
|
-
if (apps.length > 0) trackedLinks.push({ file: event.path, entities: apps });
|
|
17106
|
-
} 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 });
|
|
17107
17635
|
}
|
|
17108
17636
|
}
|
|
17109
17637
|
}
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
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);
|
|
17123
17651
|
}
|
|
17124
17652
|
}
|
|
17125
|
-
|
|
17126
|
-
|
|
17127
|
-
|
|
17128
|
-
|
|
17129
|
-
|
|
17130
|
-
|
|
17131
|
-
|
|
17132
|
-
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17138
|
-
|
|
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);
|
|
17139
17667
|
}
|
|
17140
|
-
|
|
17141
|
-
|
|
17142
|
-
|
|
17143
|
-
|
|
17144
|
-
|
|
17145
|
-
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
duration_ms: duration,
|
|
17149
|
-
success: false,
|
|
17150
|
-
files_changed: batch.events.length,
|
|
17151
|
-
changed_paths: changedPaths,
|
|
17152
|
-
error: err instanceof Error ? err.message : String(err),
|
|
17153
|
-
steps: tracker.steps
|
|
17154
|
-
});
|
|
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
|
+
}
|
|
17155
17676
|
}
|
|
17156
|
-
serverLog("watcher", `Failed to rebuild index: ${err instanceof Error ? err.message : err}`, "error");
|
|
17157
17677
|
}
|
|
17158
|
-
|
|
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,
|
|
17159
17718
|
onStateChange: (status) => {
|
|
17160
17719
|
if (status.state === "dirty") {
|
|
17161
17720
|
serverLog("watcher", "Index may be stale", "warn");
|
|
@@ -17165,6 +17724,16 @@ async function runPostIndexWork(index) {
|
|
|
17165
17724
|
serverLog("watcher", `Watcher error: ${err.message}`, "error");
|
|
17166
17725
|
}
|
|
17167
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
|
+
}
|
|
17168
17737
|
watcher.start();
|
|
17169
17738
|
serverLog("watcher", "File watcher started");
|
|
17170
17739
|
}
|