@velvetmonkey/flywheel-memory 2.0.36 → 2.0.38

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