@velvetmonkey/flywheel-memory 2.0.35 → 2.0.37

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