@velvetmonkey/flywheel-memory 2.5.12 → 2.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -344,7 +344,7 @@ function resolveWorkerPath() {
344
344
  async function initEmbeddings() {
345
345
  if (workerReady && worker) return;
346
346
  if (workerInitPromise) return workerInitPromise;
347
- workerInitPromise = new Promise((resolve3, reject) => {
347
+ workerInitPromise = new Promise((resolve4, reject) => {
348
348
  try {
349
349
  const workerPath = resolveWorkerPath();
350
350
  console.error(`[Semantic] Spawning embedding worker: ${workerPath}`);
@@ -359,7 +359,7 @@ async function initEmbeddings() {
359
359
  console.error(`[Semantic] Probed model ${activeModelConfig.id}: ${msg.dims} dims`);
360
360
  }
361
361
  console.error(`[Semantic] Worker ready (model: ${activeModelConfig.id}, dims: ${msg.dims})`);
362
- resolve3();
362
+ resolve4();
363
363
  break;
364
364
  case "result": {
365
365
  const pending = pendingEmbeds.get(msg.id);
@@ -435,8 +435,8 @@ async function embedText(text) {
435
435
  throw new Error("Embedding worker not available");
436
436
  }
437
437
  const id = ++embedRequestId;
438
- return new Promise((resolve3, reject) => {
439
- pendingEmbeds.set(id, { resolve: resolve3, reject });
438
+ return new Promise((resolve4, reject) => {
439
+ pendingEmbeds.set(id, { resolve: resolve4, reject });
440
440
  worker.postMessage({ type: "embed", id, text });
441
441
  });
442
442
  }
@@ -2696,7 +2696,7 @@ function isLockContentionError(error) {
2696
2696
  return false;
2697
2697
  }
2698
2698
  function sleep(ms) {
2699
- return new Promise((resolve3) => setTimeout(resolve3, ms));
2699
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
2700
2700
  }
2701
2701
  function calculateDelay(attempt, config2) {
2702
2702
  let delay = config2.baseDelayMs * Math.pow(2, attempt);
@@ -4304,10 +4304,26 @@ async function suggestRelatedLinks(content, options = {}) {
4304
4304
  (token) => contentTokens.has(token) || contentStems.has(stem(token))
4305
4305
  );
4306
4306
  const strongCooccurrence = boost >= config2.minCooccurrenceGate;
4307
- if (!hasContentOverlap && !strongCooccurrence) {
4307
+ let multiSeedOK = true;
4308
+ if (!hasContentOverlap && cooccurrenceIndex && config2.minContentMatch > 0) {
4309
+ let qualifyingSeedCount = 0;
4310
+ for (const seed of cooccurrenceSeeds) {
4311
+ const entityAssocs = cooccurrenceIndex.associations[seed];
4312
+ if (!entityAssocs) continue;
4313
+ const coocCount = entityAssocs.get(entityName) || 0;
4314
+ if (coocCount < (cooccurrenceIndex.minCount ?? 2)) continue;
4315
+ const dfEntity = cooccurrenceIndex.documentFrequency?.get(entityName) || 0;
4316
+ const dfSeed = cooccurrenceIndex.documentFrequency?.get(seed) || 0;
4317
+ if (dfEntity === 0 || dfSeed === 0) continue;
4318
+ const npmi = computeNpmi(coocCount, dfEntity, dfSeed, cooccurrenceIndex.totalNotesScanned ?? 1);
4319
+ if (npmi > 0) qualifyingSeedCount++;
4320
+ }
4321
+ multiSeedOK = qualifyingSeedCount >= 2;
4322
+ }
4323
+ if (!hasContentOverlap && !(strongCooccurrence && multiSeedOK)) {
4308
4324
  continue;
4309
4325
  }
4310
- if (hasContentOverlap || strongCooccurrence) {
4326
+ if (hasContentOverlap || strongCooccurrence && multiSeedOK) {
4311
4327
  entitiesWithAnyScoringPath.add(entityName);
4312
4328
  }
4313
4329
  const typeBoost = disabled.has("type_boost") ? 0 : getTypeBoost(category, getConfig()?.custom_categories, entityName);
@@ -4853,8 +4869,8 @@ var init_wikilinks = __esm({
4853
4869
  minWordLength: 3,
4854
4870
  minSuggestionScore: 10,
4855
4871
  // Exact match (10) or two stem matches
4856
- minMatchRatio: 0.4,
4857
- // 40% of multi-word entity must match
4872
+ minMatchRatio: 0.6,
4873
+ // 60% of multi-word entity must match (blocks 1-of-2 token FPs)
4858
4874
  requireMultipleMatches: false,
4859
4875
  stemMatchBonus: 5,
4860
4876
  // Standard bonus for stem matches
@@ -4863,8 +4879,10 @@ var init_wikilinks = __esm({
4863
4879
  fuzzyMatchBonus: 4,
4864
4880
  // Moderate fuzzy bonus
4865
4881
  contentRelevanceFloor: 5,
4866
- noRelevanceCap: 10,
4867
- minCooccurrenceGate: 5,
4882
+ noRelevanceCap: 9,
4883
+ // Below minSuggestionScore — graph-only entities can't reach threshold
4884
+ minCooccurrenceGate: 6,
4885
+ // Stronger graph signal for graph-only admission
4868
4886
  minContentMatch: 2
4869
4887
  },
4870
4888
  aggressive: {
@@ -5185,16 +5203,16 @@ var init_tool_embeddings_generated = __esm({
5185
5203
 
5186
5204
  // src/core/write/path-security.ts
5187
5205
  import fs20 from "fs/promises";
5188
- import path22 from "path";
5206
+ import path23 from "path";
5189
5207
  function isSensitivePath(filePath) {
5190
5208
  const normalizedPath = filePath.replace(/\\/g, "/");
5191
5209
  return SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath));
5192
5210
  }
5193
5211
  function isWithinDirectory(child, parent, allowEqual = false) {
5194
- const rel = path22.relative(path22.resolve(parent), path22.resolve(child));
5212
+ const rel = path23.relative(path23.resolve(parent), path23.resolve(child));
5195
5213
  if (rel === "") return allowEqual;
5196
- const firstSeg = rel.split(path22.sep)[0];
5197
- return firstSeg !== ".." && !path22.isAbsolute(rel);
5214
+ const firstSeg = rel.split(path23.sep)[0];
5215
+ return firstSeg !== ".." && !path23.isAbsolute(rel);
5198
5216
  }
5199
5217
  function validatePath(vaultPath2, notePath) {
5200
5218
  if (notePath.startsWith("/")) {
@@ -5206,11 +5224,11 @@ function validatePath(vaultPath2, notePath) {
5206
5224
  if (notePath.startsWith("\\")) {
5207
5225
  return false;
5208
5226
  }
5209
- return isWithinDirectory(path22.resolve(vaultPath2, notePath), vaultPath2);
5227
+ return isWithinDirectory(path23.resolve(vaultPath2, notePath), vaultPath2);
5210
5228
  }
5211
5229
  function sanitizeNotePath(notePath) {
5212
- const dir = path22.dirname(notePath);
5213
- let filename = path22.basename(notePath);
5230
+ const dir = path23.dirname(notePath);
5231
+ let filename = path23.basename(notePath);
5214
5232
  const ext = filename.endsWith(".md") ? ".md" : "";
5215
5233
  let stem2 = ext ? filename.slice(0, -ext.length) : filename;
5216
5234
  stem2 = stem2.replace(/\s+/g, "-");
@@ -5219,7 +5237,7 @@ function sanitizeNotePath(notePath) {
5219
5237
  stem2 = stem2.replace(/-{2,}/g, "-");
5220
5238
  stem2 = stem2.replace(/^-+|-+$/g, "");
5221
5239
  filename = stem2 + (ext || ".md");
5222
- return dir === "." ? filename : path22.join(dir, filename).replace(/\\/g, "/");
5240
+ return dir === "." ? filename : path23.join(dir, filename).replace(/\\/g, "/");
5223
5241
  }
5224
5242
  async function validatePathSecure(vaultPath2, notePath) {
5225
5243
  if (notePath.startsWith("/")) {
@@ -5240,14 +5258,14 @@ async function validatePathSecure(vaultPath2, notePath) {
5240
5258
  reason: "Absolute paths not allowed"
5241
5259
  };
5242
5260
  }
5243
- const firstSeg = path22.normalize(notePath).split(path22.sep).filter(Boolean)[0];
5261
+ const firstSeg = path23.normalize(notePath).split(path23.sep).filter(Boolean)[0];
5244
5262
  if (firstSeg === "..") {
5245
5263
  return {
5246
5264
  valid: false,
5247
5265
  reason: "Path traversal not allowed"
5248
5266
  };
5249
5267
  }
5250
- if (!isWithinDirectory(path22.resolve(vaultPath2, notePath), vaultPath2)) {
5268
+ if (!isWithinDirectory(path23.resolve(vaultPath2, notePath), vaultPath2)) {
5251
5269
  return {
5252
5270
  valid: false,
5253
5271
  reason: "Path traversal not allowed"
@@ -5260,7 +5278,7 @@ async function validatePathSecure(vaultPath2, notePath) {
5260
5278
  };
5261
5279
  }
5262
5280
  try {
5263
- const fullPath = path22.join(vaultPath2, notePath);
5281
+ const fullPath = path23.join(vaultPath2, notePath);
5264
5282
  try {
5265
5283
  await fs20.access(fullPath);
5266
5284
  const realPath = await fs20.realpath(fullPath);
@@ -5271,7 +5289,7 @@ async function validatePathSecure(vaultPath2, notePath) {
5271
5289
  reason: "Symlink target is outside vault"
5272
5290
  };
5273
5291
  }
5274
- const relativePath = path22.relative(realVaultPath, realPath);
5292
+ const relativePath = path23.relative(realVaultPath, realPath);
5275
5293
  if (isSensitivePath(relativePath)) {
5276
5294
  return {
5277
5295
  valid: false,
@@ -5279,7 +5297,7 @@ async function validatePathSecure(vaultPath2, notePath) {
5279
5297
  };
5280
5298
  }
5281
5299
  } catch {
5282
- const parentDir = path22.dirname(fullPath);
5300
+ const parentDir = path23.dirname(fullPath);
5283
5301
  try {
5284
5302
  await fs20.access(parentDir);
5285
5303
  const realParentPath = await fs20.realpath(parentDir);
@@ -6058,7 +6076,7 @@ var init_content_mutation = __esm({
6058
6076
 
6059
6077
  // src/core/write/file-io.ts
6060
6078
  import fs21 from "fs/promises";
6061
- import path23 from "path";
6079
+ import path24 from "path";
6062
6080
  import matter5 from "gray-matter";
6063
6081
  import { createHash as createHash2 } from "node:crypto";
6064
6082
  function computeContentHash(rawContent) {
@@ -6069,7 +6087,7 @@ async function readVaultFile(vaultPath2, notePath) {
6069
6087
  if (!validation.valid) {
6070
6088
  throw new Error(`Invalid path: ${validation.reason}`);
6071
6089
  }
6072
- const fullPath = path23.join(vaultPath2, notePath);
6090
+ const fullPath = path24.join(vaultPath2, notePath);
6073
6091
  const [rawContent, stat4] = await Promise.all([
6074
6092
  fs21.readFile(fullPath, "utf-8"),
6075
6093
  fs21.stat(fullPath)
@@ -6124,7 +6142,7 @@ async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEn
6124
6142
  if (!validation.valid) {
6125
6143
  throw new Error(`Invalid path: ${validation.reason}`);
6126
6144
  }
6127
- const fullPath = path23.join(vaultPath2, notePath);
6145
+ const fullPath = path24.join(vaultPath2, notePath);
6128
6146
  if (expectedHash) {
6129
6147
  const currentRaw = await fs21.readFile(fullPath, "utf-8");
6130
6148
  const currentHash = computeContentHash(currentRaw);
@@ -6192,8 +6210,8 @@ function createContext(variables = {}) {
6192
6210
  steps: {}
6193
6211
  };
6194
6212
  }
6195
- function resolvePath(obj, path40) {
6196
- const parts = path40.split(".");
6213
+ function resolvePath(obj, path41) {
6214
+ const parts = path41.split(".");
6197
6215
  let current = obj;
6198
6216
  for (const part of parts) {
6199
6217
  if (current === void 0 || current === null) {
@@ -6651,7 +6669,7 @@ __export(conditions_exports, {
6651
6669
  shouldStepExecute: () => shouldStepExecute
6652
6670
  });
6653
6671
  import fs29 from "fs/promises";
6654
- import path31 from "path";
6672
+ import path32 from "path";
6655
6673
  async function evaluateCondition(condition, vaultPath2, context) {
6656
6674
  const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
6657
6675
  const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
@@ -6704,7 +6722,7 @@ async function evaluateCondition(condition, vaultPath2, context) {
6704
6722
  }
6705
6723
  }
6706
6724
  async function evaluateFileExists(vaultPath2, notePath, expectExists) {
6707
- const fullPath = path31.join(vaultPath2, notePath);
6725
+ const fullPath = path32.join(vaultPath2, notePath);
6708
6726
  try {
6709
6727
  await fs29.access(fullPath);
6710
6728
  return {
@@ -6719,7 +6737,7 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
6719
6737
  }
6720
6738
  }
6721
6739
  async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
6722
- const fullPath = path31.join(vaultPath2, notePath);
6740
+ const fullPath = path32.join(vaultPath2, notePath);
6723
6741
  try {
6724
6742
  await fs29.access(fullPath);
6725
6743
  } catch {
@@ -6750,7 +6768,7 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
6750
6768
  }
6751
6769
  }
6752
6770
  async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
6753
- const fullPath = path31.join(vaultPath2, notePath);
6771
+ const fullPath = path32.join(vaultPath2, notePath);
6754
6772
  try {
6755
6773
  await fs29.access(fullPath);
6756
6774
  } catch {
@@ -6781,7 +6799,7 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
6781
6799
  }
6782
6800
  }
6783
6801
  async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
6784
- const fullPath = path31.join(vaultPath2, notePath);
6802
+ const fullPath = path32.join(vaultPath2, notePath);
6785
6803
  try {
6786
6804
  await fs29.access(fullPath);
6787
6805
  } catch {
@@ -6925,10 +6943,10 @@ var init_taskHelpers = __esm({
6925
6943
  });
6926
6944
 
6927
6945
  // src/index.ts
6928
- import * as path39 from "path";
6929
- import { readFileSync as readFileSync6, realpathSync, existsSync as existsSync3 } from "fs";
6930
- import { fileURLToPath as fileURLToPath3 } from "url";
6931
- import { dirname as dirname7, join as join20 } from "path";
6946
+ import * as path40 from "path";
6947
+ import { readFileSync as readFileSync6, realpathSync, existsSync as existsSync4 } from "fs";
6948
+ import { fileURLToPath as fileURLToPath4 } from "url";
6949
+ import { dirname as dirname8, join as join21 } from "path";
6932
6950
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6933
6951
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6934
6952
  import { performance as performance2 } from "node:perf_hooks";
@@ -7165,8 +7183,8 @@ function updateIndexProgress(parsed, total) {
7165
7183
  function normalizeTarget(target) {
7166
7184
  return target.toLowerCase().replace(/\.md$/, "");
7167
7185
  }
7168
- function normalizeNotePath(path40) {
7169
- return path40.toLowerCase().replace(/\.md$/, "");
7186
+ function normalizeNotePath(path41) {
7187
+ return path41.toLowerCase().replace(/\.md$/, "");
7170
7188
  }
7171
7189
  async function buildVaultIndex(vaultPath2, options = {}) {
7172
7190
  const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
@@ -7214,7 +7232,7 @@ async function buildVaultIndexInternal(vaultPath2, startTime, onProgress) {
7214
7232
  console.error(`Parsed ${parsedCount}/${files.length} files (${elapsed}s)`);
7215
7233
  onProgress?.(parsedCount, files.length);
7216
7234
  }
7217
- await new Promise((resolve3) => setImmediate(resolve3));
7235
+ await new Promise((resolve4) => setImmediate(resolve4));
7218
7236
  }
7219
7237
  if (parseErrors.length > 0) {
7220
7238
  const msg = `Failed to parse ${parseErrors.length} file(s):`;
@@ -7340,7 +7358,7 @@ function findSimilarEntity(index, target) {
7340
7358
  }
7341
7359
  const maxDist = normalizedLen <= 10 ? 1 : 2;
7342
7360
  let bestMatch;
7343
- for (const [entity, path40] of index.entities) {
7361
+ for (const [entity, path41] of index.entities) {
7344
7362
  const lenDiff = Math.abs(entity.length - normalizedLen);
7345
7363
  if (lenDiff > maxDist) {
7346
7364
  continue;
@@ -7348,7 +7366,7 @@ function findSimilarEntity(index, target) {
7348
7366
  const dist = levenshteinDistance(normalized, entity);
7349
7367
  if (dist > 0 && dist <= maxDist) {
7350
7368
  if (!bestMatch || dist < bestMatch.distance) {
7351
- bestMatch = { path: path40, entity, distance: dist };
7369
+ bestMatch = { path: path41, entity, distance: dist };
7352
7370
  if (dist === 1) {
7353
7371
  return bestMatch;
7354
7372
  }
@@ -7907,30 +7925,30 @@ var EventQueue = class {
7907
7925
  * Add a new event to the queue
7908
7926
  */
7909
7927
  push(type, rawPath) {
7910
- const path40 = normalizePath(rawPath);
7928
+ const path41 = normalizePath(rawPath);
7911
7929
  const now = Date.now();
7912
7930
  const event = {
7913
7931
  type,
7914
- path: path40,
7932
+ path: path41,
7915
7933
  timestamp: now
7916
7934
  };
7917
- let pending = this.pending.get(path40);
7935
+ let pending = this.pending.get(path41);
7918
7936
  if (!pending) {
7919
7937
  pending = {
7920
7938
  events: [],
7921
7939
  timer: null,
7922
7940
  lastEvent: now
7923
7941
  };
7924
- this.pending.set(path40, pending);
7942
+ this.pending.set(path41, pending);
7925
7943
  }
7926
7944
  pending.events.push(event);
7927
7945
  pending.lastEvent = now;
7928
- console.error(`[flywheel] QUEUE: pushed ${type} for ${path40}, pending=${this.pending.size}`);
7946
+ console.error(`[flywheel] QUEUE: pushed ${type} for ${path41}, pending=${this.pending.size}`);
7929
7947
  if (pending.timer) {
7930
7948
  clearTimeout(pending.timer);
7931
7949
  }
7932
7950
  pending.timer = setTimeout(() => {
7933
- this.flushPath(path40);
7951
+ this.flushPath(path41);
7934
7952
  }, this.config.debounceMs);
7935
7953
  if (this.pending.size >= this.config.batchSize) {
7936
7954
  this.flush();
@@ -7951,10 +7969,10 @@ var EventQueue = class {
7951
7969
  /**
7952
7970
  * Flush a single path's events
7953
7971
  */
7954
- flushPath(path40) {
7955
- const pending = this.pending.get(path40);
7972
+ flushPath(path41) {
7973
+ const pending = this.pending.get(path41);
7956
7974
  if (!pending || pending.events.length === 0) return;
7957
- console.error(`[flywheel] QUEUE: flushing ${path40}, events=${pending.events.length}`);
7975
+ console.error(`[flywheel] QUEUE: flushing ${path41}, events=${pending.events.length}`);
7958
7976
  if (pending.timer) {
7959
7977
  clearTimeout(pending.timer);
7960
7978
  pending.timer = null;
@@ -7963,7 +7981,7 @@ var EventQueue = class {
7963
7981
  if (coalescedType) {
7964
7982
  const coalesced = {
7965
7983
  type: coalescedType,
7966
- path: path40,
7984
+ path: path41,
7967
7985
  originalEvents: [...pending.events]
7968
7986
  };
7969
7987
  this.onBatch({
@@ -7972,7 +7990,7 @@ var EventQueue = class {
7972
7990
  timestamp: Date.now()
7973
7991
  });
7974
7992
  }
7975
- this.pending.delete(path40);
7993
+ this.pending.delete(path41);
7976
7994
  }
7977
7995
  /**
7978
7996
  * Flush all pending events
@@ -7984,7 +8002,7 @@ var EventQueue = class {
7984
8002
  }
7985
8003
  if (this.pending.size === 0) return;
7986
8004
  const events = [];
7987
- for (const [path40, pending] of this.pending) {
8005
+ for (const [path41, pending] of this.pending) {
7988
8006
  if (pending.timer) {
7989
8007
  clearTimeout(pending.timer);
7990
8008
  }
@@ -7992,7 +8010,7 @@ var EventQueue = class {
7992
8010
  if (coalescedType) {
7993
8011
  events.push({
7994
8012
  type: coalescedType,
7995
- path: path40,
8013
+ path: path41,
7996
8014
  originalEvents: [...pending.events]
7997
8015
  });
7998
8016
  }
@@ -8292,7 +8310,7 @@ async function processBatch(index, vaultPath2, batch, options = {}) {
8292
8310
  }
8293
8311
  onProgress?.(processed, total);
8294
8312
  if (processed % YIELD_INTERVAL === 0 && processed < total) {
8295
- await new Promise((resolve3) => setImmediate(resolve3));
8313
+ await new Promise((resolve4) => setImmediate(resolve4));
8296
8314
  }
8297
8315
  }
8298
8316
  const durationMs = Date.now() - startTime;
@@ -8384,31 +8402,31 @@ function createVaultWatcher(options) {
8384
8402
  usePolling: config2.usePolling,
8385
8403
  interval: config2.usePolling ? config2.pollInterval : void 0
8386
8404
  });
8387
- watcher.on("add", (path40) => {
8388
- console.error(`[flywheel] RAW EVENT: add ${path40}`);
8389
- if (shouldWatch(path40, vaultPath2)) {
8390
- console.error(`[flywheel] ACCEPTED: add ${path40}`);
8391
- eventQueue.push("add", path40);
8405
+ watcher.on("add", (path41) => {
8406
+ console.error(`[flywheel] RAW EVENT: add ${path41}`);
8407
+ if (shouldWatch(path41, vaultPath2)) {
8408
+ console.error(`[flywheel] ACCEPTED: add ${path41}`);
8409
+ eventQueue.push("add", path41);
8392
8410
  } else {
8393
- console.error(`[flywheel] FILTERED: add ${path40}`);
8411
+ console.error(`[flywheel] FILTERED: add ${path41}`);
8394
8412
  }
8395
8413
  });
8396
- watcher.on("change", (path40) => {
8397
- console.error(`[flywheel] RAW EVENT: change ${path40}`);
8398
- if (shouldWatch(path40, vaultPath2)) {
8399
- console.error(`[flywheel] ACCEPTED: change ${path40}`);
8400
- eventQueue.push("change", path40);
8414
+ watcher.on("change", (path41) => {
8415
+ console.error(`[flywheel] RAW EVENT: change ${path41}`);
8416
+ if (shouldWatch(path41, vaultPath2)) {
8417
+ console.error(`[flywheel] ACCEPTED: change ${path41}`);
8418
+ eventQueue.push("change", path41);
8401
8419
  } else {
8402
- console.error(`[flywheel] FILTERED: change ${path40}`);
8420
+ console.error(`[flywheel] FILTERED: change ${path41}`);
8403
8421
  }
8404
8422
  });
8405
- watcher.on("unlink", (path40) => {
8406
- console.error(`[flywheel] RAW EVENT: unlink ${path40}`);
8407
- if (shouldWatch(path40, vaultPath2)) {
8408
- console.error(`[flywheel] ACCEPTED: unlink ${path40}`);
8409
- eventQueue.push("unlink", path40);
8423
+ watcher.on("unlink", (path41) => {
8424
+ console.error(`[flywheel] RAW EVENT: unlink ${path41}`);
8425
+ if (shouldWatch(path41, vaultPath2)) {
8426
+ console.error(`[flywheel] ACCEPTED: unlink ${path41}`);
8427
+ eventQueue.push("unlink", path41);
8410
8428
  } else {
8411
- console.error(`[flywheel] FILTERED: unlink ${path40}`);
8429
+ console.error(`[flywheel] FILTERED: unlink ${path41}`);
8412
8430
  }
8413
8431
  });
8414
8432
  watcher.on("ready", () => {
@@ -8448,8 +8466,6 @@ import {
8448
8466
  getProtectedZones,
8449
8467
  rangeOverlapsProtectedZone,
8450
8468
  detectImplicitEntities as detectImplicitEntities2,
8451
- checkDbIntegrity,
8452
- safeBackupAsync,
8453
8469
  recordEntityMention as recordEntityMention2
8454
8470
  } from "@velvetmonkey/vault-core";
8455
8471
  init_serverLog();
@@ -9740,6 +9756,17 @@ var PipelineRunner = class {
9740
9756
  suggestionResults = [];
9741
9757
  async run() {
9742
9758
  const { p, tracker } = this;
9759
+ if (p.ctx.integrityState === "failed") {
9760
+ serverLog("watcher", `Skipping batch for ${p.ctx.name}: StateDb integrity failed`, "warn");
9761
+ this.activity.busy = false;
9762
+ this.activity.current_step = null;
9763
+ this.activity.last_completed_at = Date.now();
9764
+ this.activity.last_completed_trigger = "watcher";
9765
+ this.activity.last_completed_duration_ms = 0;
9766
+ this.activity.last_completed_files = p.events.length;
9767
+ this.activity.last_completed_steps = [];
9768
+ return;
9769
+ }
9743
9770
  this.activity.busy = true;
9744
9771
  this.activity.trigger = "watcher";
9745
9772
  this.activity.started_at = this.batchStart;
@@ -10625,21 +10652,15 @@ var PipelineRunner = class {
10625
10652
  async integrityCheck() {
10626
10653
  const { p } = this;
10627
10654
  if (!p.sd) return { skipped: true, reason: "no statedb" };
10628
- const INTEGRITY_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
10629
- const lastCheckRow = p.sd.getMetadataValue.get("last_integrity_check");
10630
- const lastCheck = lastCheckRow ? parseInt(lastCheckRow.value, 10) : 0;
10631
- if (Date.now() - lastCheck < INTEGRITY_CHECK_INTERVAL_MS) {
10632
- return { skipped: true, reason: "checked recently" };
10633
- }
10634
- const result = checkDbIntegrity(p.sd.db);
10635
- p.sd.setMetadataValue.run("last_integrity_check", String(Date.now()));
10636
- if (result.ok) {
10637
- await safeBackupAsync(p.sd.db, p.sd.dbPath);
10638
- return { integrity: "ok", backed_up: true };
10639
- } else {
10655
+ const result = await p.runIntegrityCheck(p.ctx, "watcher");
10656
+ if (result.status === "healthy") {
10657
+ return { integrity: "ok", backed_up: result.backupCreated };
10658
+ }
10659
+ if (result.status === "failed") {
10640
10660
  serverLog("watcher", `Integrity check FAILED: ${result.detail}`, "error");
10641
10661
  return { integrity: "failed", detail: result.detail };
10642
10662
  }
10663
+ return { skipped: true, reason: result.detail ?? "integrity runner unavailable" };
10643
10664
  }
10644
10665
  // ── Maintenance: periodic incremental vacuum ─────────────────────
10645
10666
  async maintenance() {
@@ -10999,8 +11020,77 @@ function getToolSelectionReport(stateDb2, daysBack = 7) {
10999
11020
  };
11000
11021
  }
11001
11022
 
11023
+ // src/core/read/integrity.ts
11024
+ import * as path16 from "node:path";
11025
+ import { existsSync as existsSync3 } from "node:fs";
11026
+ import { Worker as Worker2 } from "node:worker_threads";
11027
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
11028
+ var INTEGRITY_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
11029
+ var INTEGRITY_CHECK_TIMEOUT_MS = 2 * 60 * 1e3;
11030
+ var INTEGRITY_BACKUP_INTERVAL_MS = 6 * 60 * 60 * 1e3;
11031
+ var INTEGRITY_METADATA_KEYS = {
11032
+ checkedAt: "last_integrity_check",
11033
+ status: "last_integrity_status",
11034
+ durationMs: "last_integrity_duration_ms",
11035
+ detail: "last_integrity_detail"
11036
+ };
11037
+ function resolveWorkerSpec() {
11038
+ const thisFile = fileURLToPath2(import.meta.url);
11039
+ const thisDir = path16.dirname(thisFile);
11040
+ const prodPath = path16.join(thisDir, "integrity-worker.js");
11041
+ if (existsSync3(prodPath)) return { filename: prodPath };
11042
+ const distPath = path16.resolve(thisDir, "..", "..", "..", "dist", "integrity-worker.js");
11043
+ if (existsSync3(distPath)) return { filename: distPath };
11044
+ const srcPath = path16.join(thisDir, "integrity-worker.ts");
11045
+ return { filename: srcPath, execArgv: ["--import", "tsx"] };
11046
+ }
11047
+ async function runIntegrityWorker(message, timeoutMs = INTEGRITY_CHECK_TIMEOUT_MS) {
11048
+ const workerSpec = resolveWorkerSpec();
11049
+ return new Promise((resolve4) => {
11050
+ const worker2 = new Worker2(workerSpec.filename, {
11051
+ execArgv: workerSpec.execArgv
11052
+ });
11053
+ let settled = false;
11054
+ const finish = (result) => {
11055
+ if (settled) return;
11056
+ settled = true;
11057
+ clearTimeout(timer2);
11058
+ void worker2.terminate().catch(() => {
11059
+ });
11060
+ resolve4(result);
11061
+ };
11062
+ const timer2 = setTimeout(() => {
11063
+ finish({
11064
+ status: "error",
11065
+ detail: `Integrity worker timed out after ${timeoutMs}ms`,
11066
+ durationMs: timeoutMs,
11067
+ backupCreated: false
11068
+ });
11069
+ }, timeoutMs);
11070
+ worker2.once("message", (result) => finish(result));
11071
+ worker2.once("error", (err) => {
11072
+ finish({
11073
+ status: "error",
11074
+ detail: err.message,
11075
+ durationMs: 0,
11076
+ backupCreated: false
11077
+ });
11078
+ });
11079
+ worker2.once("exit", (code) => {
11080
+ if (settled || code === 0) return;
11081
+ finish({
11082
+ status: "error",
11083
+ detail: `Integrity worker exited with code ${code}`,
11084
+ durationMs: 0,
11085
+ backupCreated: false
11086
+ });
11087
+ });
11088
+ worker2.postMessage(message);
11089
+ });
11090
+ }
11091
+
11002
11092
  // src/index.ts
11003
- import { openStateDb, scanVaultEntities as scanVaultEntities5, getAllEntitiesFromDb as getAllEntitiesFromDb6, loadContentHashes, saveContentHashBatch, renameContentHash, checkDbIntegrity as checkDbIntegrity2, safeBackupAsync as safeBackupAsync2, preserveCorruptedDb, deleteStateDbFiles, attemptSalvage } from "@velvetmonkey/vault-core";
11093
+ import { openStateDb, scanVaultEntities as scanVaultEntities5, getAllEntitiesFromDb as getAllEntitiesFromDb6, loadContentHashes, saveContentHashBatch, renameContentHash } from "@velvetmonkey/vault-core";
11004
11094
 
11005
11095
  // src/core/write/memory.ts
11006
11096
  init_wikilinkFeedback();
@@ -12074,8 +12164,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
12074
12164
  }
12075
12165
  }
12076
12166
  }
12077
- return Array.from(noteMap.entries()).map(([path40, stats]) => ({
12078
- path: path40,
12167
+ return Array.from(noteMap.entries()).map(([path41, stats]) => ({
12168
+ path: path41,
12079
12169
  access_count: stats.access_count,
12080
12170
  last_accessed: stats.last_accessed,
12081
12171
  tools_used: Array.from(stats.tools)
@@ -12762,10 +12852,10 @@ Use "flywheel_config" to inspect runtime configuration and set "tool_tier_overri
12762
12852
  }
12763
12853
 
12764
12854
  // src/tool-registry.ts
12765
- import * as path38 from "path";
12766
- import { dirname as dirname5, join as join18 } from "path";
12855
+ import * as path39 from "path";
12856
+ import { dirname as dirname6, join as join19 } from "path";
12767
12857
  import { statSync as statSync6, readFileSync as readFileSync5 } from "fs";
12768
- import { fileURLToPath as fileURLToPath2 } from "url";
12858
+ import { fileURLToPath as fileURLToPath3 } from "url";
12769
12859
  import { z as z39 } from "zod";
12770
12860
  import { CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
12771
12861
  import { getSessionId } from "@velvetmonkey/vault-core";
@@ -13114,13 +13204,13 @@ function multiHopBackfill(primaryResults, index, stateDb2, config2 = {}) {
13114
13204
  candidates.sort((a, b) => b.score - a.score);
13115
13205
  return candidates.slice(0, cfg.maxBackfill).map((c) => c.result);
13116
13206
  }
13117
- function scoreCandidate(path40, index, stateDb2) {
13118
- const note = index.notes.get(path40);
13207
+ function scoreCandidate(path41, index, stateDb2) {
13208
+ const note = index.notes.get(path41);
13119
13209
  const decay = recencyDecay(note?.modified);
13120
13210
  let hubScore = 1;
13121
13211
  if (stateDb2) {
13122
13212
  try {
13123
- const title = note?.title ?? path40.replace(/\.md$/, "").split("/").pop() ?? "";
13213
+ const title = note?.title ?? path41.replace(/\.md$/, "").split("/").pop() ?? "";
13124
13214
  const entity = getEntityByName3(stateDb2, title);
13125
13215
  if (entity) hubScore = entity.hubScore ?? 1;
13126
13216
  } catch {
@@ -13357,7 +13447,7 @@ init_stemmer();
13357
13447
 
13358
13448
  // src/tools/read/structure.ts
13359
13449
  import * as fs12 from "fs";
13360
- import * as path16 from "path";
13450
+ import * as path17 from "path";
13361
13451
  var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
13362
13452
  function extractHeadings2(content) {
13363
13453
  const lines = content.replace(/\r\n/g, "\n").split("\n");
@@ -13411,7 +13501,7 @@ function buildSections(headings, totalLines) {
13411
13501
  async function getNoteStructure(index, notePath, vaultPath2) {
13412
13502
  const note = index.notes.get(notePath);
13413
13503
  if (!note) return null;
13414
- const absolutePath = path16.join(vaultPath2, notePath);
13504
+ const absolutePath = path17.join(vaultPath2, notePath);
13415
13505
  let content;
13416
13506
  try {
13417
13507
  content = await fs12.promises.readFile(absolutePath, "utf-8");
@@ -13435,7 +13525,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
13435
13525
  async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
13436
13526
  const note = index.notes.get(notePath);
13437
13527
  if (!note) return null;
13438
- const absolutePath = path16.join(vaultPath2, notePath);
13528
+ const absolutePath = path17.join(vaultPath2, notePath);
13439
13529
  let content;
13440
13530
  try {
13441
13531
  content = await fs12.promises.readFile(absolutePath, "utf-8");
@@ -13478,7 +13568,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
13478
13568
  const results = [];
13479
13569
  for (const note of index.notes.values()) {
13480
13570
  if (folder && !note.path.startsWith(folder)) continue;
13481
- const absolutePath = path16.join(vaultPath2, note.path);
13571
+ const absolutePath = path17.join(vaultPath2, note.path);
13482
13572
  let content;
13483
13573
  try {
13484
13574
  content = await fs12.promises.readFile(absolutePath, "utf-8");
@@ -13580,11 +13670,11 @@ function applyEntityBridging(results, stateDb2, maxBridgesPerResult = 5) {
13580
13670
  const linkMap = /* @__PURE__ */ new Map();
13581
13671
  try {
13582
13672
  const paths = results.map((r) => r.path).filter(Boolean);
13583
- for (const path40 of paths) {
13673
+ for (const path41 of paths) {
13584
13674
  const rows = stateDb2.db.prepare(
13585
13675
  "SELECT target FROM note_links WHERE note_path = ?"
13586
- ).all(path40);
13587
- linkMap.set(path40, new Set(rows.map((r) => r.target)));
13676
+ ).all(path41);
13677
+ linkMap.set(path41, new Set(rows.map((r) => r.target)));
13588
13678
  }
13589
13679
  } catch {
13590
13680
  return;
@@ -14137,7 +14227,7 @@ init_vault_scope();
14137
14227
 
14138
14228
  // src/tools/read/graph.ts
14139
14229
  import * as fs13 from "fs";
14140
- import * as path17 from "path";
14230
+ import * as path18 from "path";
14141
14231
  import { z as z2 } from "zod";
14142
14232
 
14143
14233
  // src/tools/read/graphAdvanced.ts
@@ -14571,7 +14661,7 @@ function detectCycles(index, maxLength = 10, limit = 20) {
14571
14661
  // src/tools/read/graph.ts
14572
14662
  async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
14573
14663
  try {
14574
- const fullPath = path17.join(vaultPath2, sourcePath);
14664
+ const fullPath = path18.join(vaultPath2, sourcePath);
14575
14665
  const content = await fs13.promises.readFile(fullPath, "utf-8");
14576
14666
  const allLines = content.split("\n");
14577
14667
  let fmLines = 0;
@@ -15116,14 +15206,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath, getStateDb4 = ()
15116
15206
  };
15117
15207
  function findSimilarEntity2(target, entities) {
15118
15208
  const targetLower = target.toLowerCase();
15119
- for (const [name, path40] of entities) {
15209
+ for (const [name, path41] of entities) {
15120
15210
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
15121
- return path40;
15211
+ return path41;
15122
15212
  }
15123
15213
  }
15124
- for (const [name, path40] of entities) {
15214
+ for (const [name, path41] of entities) {
15125
15215
  if (name.includes(targetLower) || targetLower.includes(name)) {
15126
- return path40;
15216
+ return path41;
15127
15217
  }
15128
15218
  }
15129
15219
  return void 0;
@@ -15666,7 +15756,17 @@ function getProactiveLinkingOneLiner(stateDb2, daysBack = 1) {
15666
15756
  init_wikilinkFeedback();
15667
15757
  init_embeddings();
15668
15758
  var STALE_THRESHOLD_SECONDS = 300;
15669
- function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb4 = () => null, getWatcherStatus2 = () => null, getVersion = () => "unknown", getPipelineActivityState = () => null) {
15759
+ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb4 = () => null, getWatcherStatus2 = () => null, getVersion = () => "unknown", getPipelineActivityState = () => null, getVaultRuntimeState = () => ({
15760
+ bootState: "booting",
15761
+ integrityState: "unknown",
15762
+ integrityCheckInProgress: false,
15763
+ integrityStartedAt: null,
15764
+ integritySource: null,
15765
+ lastIntegrityCheckedAt: null,
15766
+ lastIntegrityDurationMs: null,
15767
+ lastIntegrityDetail: null,
15768
+ lastBackupAt: null
15769
+ })) {
15670
15770
  const IndexProgressSchema = z4.object({
15671
15771
  parsed: z4.coerce.number().describe("Number of files parsed so far"),
15672
15772
  total: z4.coerce.number().describe("Total number of files to parse")
@@ -15819,18 +15919,14 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15819
15919
  }
15820
15920
  let dbIntegrityFailed = false;
15821
15921
  const stateDb2 = getStateDb4();
15822
- if (stateDb2) {
15823
- try {
15824
- const result = stateDb2.db.pragma("quick_check");
15825
- const ok = result.length === 1 && Object.values(result[0])[0] === "ok";
15826
- if (!ok) {
15827
- dbIntegrityFailed = true;
15828
- recommendations.push(`Database integrity check failed: ${Object.values(result[0])[0] ?? "unknown error"}`);
15829
- }
15830
- } catch (err) {
15831
- dbIntegrityFailed = true;
15832
- recommendations.push(`Database integrity check error: ${err instanceof Error ? err.message : err}`);
15833
- }
15922
+ const runtimeState = getVaultRuntimeState();
15923
+ if (runtimeState.integrityState === "failed") {
15924
+ dbIntegrityFailed = true;
15925
+ recommendations.push(`Database integrity check failed: ${runtimeState.lastIntegrityDetail ?? "unknown integrity failure"}`);
15926
+ } else if (runtimeState.integrityState === "error") {
15927
+ recommendations.push(`Database integrity check error: ${runtimeState.lastIntegrityDetail ?? "integrity runner error"}`);
15928
+ } else if (runtimeState.integrityCheckInProgress) {
15929
+ recommendations.push("Database integrity check is still running.");
15834
15930
  }
15835
15931
  const indexBuilt = indexState2 === "ready" && index !== void 0 && index.notes !== void 0;
15836
15932
  let lastIndexActivityAt;
@@ -16024,6 +16120,14 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16024
16120
  tasks_ready: isTaskCacheReady(),
16025
16121
  tasks_building: isTaskCacheBuilding(),
16026
16122
  watcher_state: getWatcherStatus2()?.state,
16123
+ boot_state: runtimeState.bootState,
16124
+ integrity_state: runtimeState.integrityState,
16125
+ integrity_check_in_progress: runtimeState.integrityCheckInProgress,
16126
+ integrity_started_at: runtimeState.integrityStartedAt,
16127
+ integrity_source: runtimeState.integritySource,
16128
+ integrity_last_checked_at: runtimeState.lastIntegrityCheckedAt,
16129
+ integrity_duration_ms: runtimeState.lastIntegrityDurationMs,
16130
+ integrity_detail: runtimeState.lastIntegrityDetail,
16027
16131
  watcher_pending: getWatcherStatus2()?.pendingEvents,
16028
16132
  last_index_activity_at: lastIndexActivityAt,
16029
16133
  last_index_activity_ago_seconds: lastIndexActivityAt ? Math.floor((Date.now() - lastIndexActivityAt) / 1e3) : void 0,
@@ -16075,6 +16179,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16075
16179
  async ({ detail = false }) => {
16076
16180
  const activity = getPipelineActivityState();
16077
16181
  const now = Date.now();
16182
+ const runtimeState = getVaultRuntimeState();
16078
16183
  const output = {
16079
16184
  busy: activity?.busy ?? false,
16080
16185
  trigger: activity?.trigger ?? null,
@@ -16083,6 +16188,9 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16083
16188
  current_step: activity?.current_step ?? null,
16084
16189
  progress: activity && activity.busy && activity.total_steps > 0 ? `${activity.completed_steps}/${activity.total_steps} steps` : null,
16085
16190
  pending_events: activity?.pending_events ?? 0,
16191
+ boot_state: runtimeState.bootState,
16192
+ integrity_state: runtimeState.integrityState,
16193
+ integrity_check_in_progress: runtimeState.integrityCheckInProgress,
16086
16194
  last_completed: activity?.last_completed_at ? {
16087
16195
  at: activity.last_completed_at,
16088
16196
  ago_seconds: Math.floor((now - activity.last_completed_at) / 1e3),
@@ -16143,8 +16251,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16143
16251
  daily_counts: z4.record(z4.number())
16144
16252
  }).describe("Activity summary for the last 7 days")
16145
16253
  };
16146
- function isPeriodicNote3(path40) {
16147
- const filename = path40.split("/").pop() || "";
16254
+ function isPeriodicNote3(path41) {
16255
+ const filename = path41.split("/").pop() || "";
16148
16256
  const nameWithoutExt = filename.replace(/\.md$/, "");
16149
16257
  const patterns = [
16150
16258
  /^\d{4}-\d{2}-\d{2}$/,
@@ -16159,7 +16267,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16159
16267
  // YYYY (yearly)
16160
16268
  ];
16161
16269
  const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
16162
- const folder = path40.split("/")[0]?.toLowerCase() || "";
16270
+ const folder = path41.split("/")[0]?.toLowerCase() || "";
16163
16271
  return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
16164
16272
  }
16165
16273
  async function runVaultStats() {
@@ -17266,13 +17374,13 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17266
17374
  max_content_chars: z6.number().default(2e4).describe("Max total chars of section content to include. Sections are truncated at paragraph boundaries.")
17267
17375
  }
17268
17376
  },
17269
- async ({ path: path40, include_content, max_content_chars }) => {
17377
+ async ({ path: path41, include_content, max_content_chars }) => {
17270
17378
  const index = getIndex();
17271
17379
  const vaultPath2 = getVaultPath();
17272
- const result = await getNoteStructure(index, path40, vaultPath2);
17380
+ const result = await getNoteStructure(index, path41, vaultPath2);
17273
17381
  if (!result) {
17274
17382
  return {
17275
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path40 }, null, 2) }]
17383
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path41 }, null, 2) }]
17276
17384
  };
17277
17385
  }
17278
17386
  let totalChars = 0;
@@ -17283,7 +17391,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17283
17391
  truncated = true;
17284
17392
  break;
17285
17393
  }
17286
- const sectionResult = await getSectionContent(index, path40, section.heading.text, vaultPath2, true);
17394
+ const sectionResult = await getSectionContent(index, path41, section.heading.text, vaultPath2, true);
17287
17395
  if (sectionResult) {
17288
17396
  let content = sectionResult.content;
17289
17397
  const remaining = max_content_chars - totalChars;
@@ -17298,13 +17406,13 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17298
17406
  }
17299
17407
  }
17300
17408
  }
17301
- const note = index.notes.get(path40);
17409
+ const note = index.notes.get(path41);
17302
17410
  const enriched = { ...result };
17303
17411
  if (note) {
17304
17412
  enriched.frontmatter = note.frontmatter;
17305
17413
  enriched.tags = note.tags;
17306
17414
  enriched.aliases = note.aliases;
17307
- const normalizedPath = path40.toLowerCase().replace(/\.md$/, "");
17415
+ const normalizedPath = path41.toLowerCase().replace(/\.md$/, "");
17308
17416
  const backlinks = index.backlinks.get(normalizedPath) || [];
17309
17417
  enriched.backlink_count = backlinks.length;
17310
17418
  enriched.outlink_count = note.outlinks.length;
@@ -17342,15 +17450,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17342
17450
  max_content_chars: z6.number().default(1e4).describe("Max chars of section content. Truncated at paragraph boundaries.")
17343
17451
  }
17344
17452
  },
17345
- async ({ path: path40, heading, include_subheadings, max_content_chars }) => {
17453
+ async ({ path: path41, heading, include_subheadings, max_content_chars }) => {
17346
17454
  const index = getIndex();
17347
17455
  const vaultPath2 = getVaultPath();
17348
- const result = await getSectionContent(index, path40, heading, vaultPath2, include_subheadings);
17456
+ const result = await getSectionContent(index, path41, heading, vaultPath2, include_subheadings);
17349
17457
  if (!result) {
17350
17458
  return {
17351
17459
  content: [{ type: "text", text: JSON.stringify({
17352
17460
  error: "Section not found",
17353
- path: path40,
17461
+ path: path41,
17354
17462
  heading
17355
17463
  }, null, 2) }]
17356
17464
  };
@@ -17411,16 +17519,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17411
17519
  offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
17412
17520
  }
17413
17521
  },
17414
- async ({ path: path40, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
17522
+ async ({ path: path41, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
17415
17523
  const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
17416
17524
  const index = getIndex();
17417
17525
  const vaultPath2 = getVaultPath();
17418
17526
  const config2 = getConfig2();
17419
- if (path40) {
17420
- const result2 = await getTasksFromNote(index, path40, vaultPath2, getExcludeTags(config2));
17527
+ if (path41) {
17528
+ const result2 = await getTasksFromNote(index, path41, vaultPath2, getExcludeTags(config2));
17421
17529
  if (!result2) {
17422
17530
  return {
17423
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path40 }, null, 2) }]
17531
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path41 }, null, 2) }]
17424
17532
  };
17425
17533
  }
17426
17534
  let filtered = result2;
@@ -17430,7 +17538,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17430
17538
  const paged2 = filtered.slice(offset, offset + limit);
17431
17539
  return {
17432
17540
  content: [{ type: "text", text: JSON.stringify({
17433
- path: path40,
17541
+ path: path41,
17434
17542
  total_count: filtered.length,
17435
17543
  returned_count: paged2.length,
17436
17544
  open: result2.filter((t) => t.status === "open").length,
@@ -17586,7 +17694,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17586
17694
  // src/tools/read/migrations.ts
17587
17695
  import { z as z7 } from "zod";
17588
17696
  import * as fs15 from "fs/promises";
17589
- import * as path18 from "path";
17697
+ import * as path19 from "path";
17590
17698
  import matter2 from "gray-matter";
17591
17699
  function getNotesInFolder(index, folder) {
17592
17700
  const notes = [];
@@ -17599,7 +17707,7 @@ function getNotesInFolder(index, folder) {
17599
17707
  return notes;
17600
17708
  }
17601
17709
  async function readFileContent(notePath, vaultPath2) {
17602
- const fullPath = path18.join(vaultPath2, notePath);
17710
+ const fullPath = path19.join(vaultPath2, notePath);
17603
17711
  try {
17604
17712
  return await fs15.readFile(fullPath, "utf-8");
17605
17713
  } catch {
@@ -17607,7 +17715,7 @@ async function readFileContent(notePath, vaultPath2) {
17607
17715
  }
17608
17716
  }
17609
17717
  async function writeFileContent(notePath, vaultPath2, content) {
17610
- const fullPath = path18.join(vaultPath2, notePath);
17718
+ const fullPath = path19.join(vaultPath2, notePath);
17611
17719
  try {
17612
17720
  await fs15.writeFile(fullPath, content, "utf-8");
17613
17721
  return true;
@@ -17788,7 +17896,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
17788
17896
 
17789
17897
  // src/tools/read/graphAnalysis.ts
17790
17898
  import fs16 from "node:fs";
17791
- import path19 from "node:path";
17899
+ import path20 from "node:path";
17792
17900
  import { z as z8 } from "zod";
17793
17901
 
17794
17902
  // src/tools/read/schema.ts
@@ -18314,7 +18422,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb4
18314
18422
  const scored = allNotes.map((note) => {
18315
18423
  let wordCount = 0;
18316
18424
  try {
18317
- const content = fs16.readFileSync(path19.join(vaultPath2, note.path), "utf-8");
18425
+ const content = fs16.readFileSync(path20.join(vaultPath2, note.path), "utf-8");
18318
18426
  const body = content.replace(/^---[\s\S]*?---\n?/, "");
18319
18427
  wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
18320
18428
  } catch {
@@ -18945,12 +19053,12 @@ import { z as z11 } from "zod";
18945
19053
 
18946
19054
  // src/tools/read/bidirectional.ts
18947
19055
  import * as fs17 from "fs/promises";
18948
- import * as path20 from "path";
19056
+ import * as path21 from "path";
18949
19057
  import matter3 from "gray-matter";
18950
19058
  var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
18951
19059
  var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
18952
19060
  async function readFileContent2(notePath, vaultPath2) {
18953
- const fullPath = path20.join(vaultPath2, notePath);
19061
+ const fullPath = path21.join(vaultPath2, notePath);
18954
19062
  try {
18955
19063
  return await fs17.readFile(fullPath, "utf-8");
18956
19064
  } catch {
@@ -19129,10 +19237,10 @@ async function suggestWikilinksInFrontmatter(index, notePath, vaultPath2) {
19129
19237
 
19130
19238
  // src/tools/read/computed.ts
19131
19239
  import * as fs18 from "fs/promises";
19132
- import * as path21 from "path";
19240
+ import * as path22 from "path";
19133
19241
  import matter4 from "gray-matter";
19134
19242
  async function readFileContent3(notePath, vaultPath2) {
19135
- const fullPath = path21.join(vaultPath2, notePath);
19243
+ const fullPath = path22.join(vaultPath2, notePath);
19136
19244
  try {
19137
19245
  return await fs18.readFile(fullPath, "utf-8");
19138
19246
  } catch {
@@ -19140,7 +19248,7 @@ async function readFileContent3(notePath, vaultPath2) {
19140
19248
  }
19141
19249
  }
19142
19250
  async function getFileStats(notePath, vaultPath2) {
19143
- const fullPath = path21.join(vaultPath2, notePath);
19251
+ const fullPath = path22.join(vaultPath2, notePath);
19144
19252
  try {
19145
19253
  const stats = await fs18.stat(fullPath);
19146
19254
  return {
@@ -19413,7 +19521,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath, getConfi
19413
19521
  init_writer();
19414
19522
  import { z as z12 } from "zod";
19415
19523
  import fs24 from "fs/promises";
19416
- import path26 from "path";
19524
+ import path27 from "path";
19417
19525
 
19418
19526
  // src/core/write/validator.ts
19419
19527
  var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
@@ -19632,16 +19740,16 @@ init_writer();
19632
19740
  init_wikilinks();
19633
19741
  init_wikilinkFeedback();
19634
19742
  import fs23 from "fs/promises";
19635
- import path25 from "path";
19743
+ import path26 from "path";
19636
19744
 
19637
19745
  // src/core/write/policy/policyPaths.ts
19638
19746
  import fs22 from "fs/promises";
19639
- import path24 from "path";
19747
+ import path25 from "path";
19640
19748
  function getPoliciesDir(vaultPath2) {
19641
- return path24.join(vaultPath2, ".flywheel", "policies");
19749
+ return path25.join(vaultPath2, ".flywheel", "policies");
19642
19750
  }
19643
19751
  function getLegacyPoliciesDir(vaultPath2) {
19644
- return path24.join(vaultPath2, ".claude", "policies");
19752
+ return path25.join(vaultPath2, ".claude", "policies");
19645
19753
  }
19646
19754
  async function ensurePoliciesDir(vaultPath2) {
19647
19755
  const dir = getPoliciesDir(vaultPath2);
@@ -19658,14 +19766,14 @@ async function migratePoliciesIfNeeded(vaultPath2) {
19658
19766
  const yamlFiles = files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
19659
19767
  if (yamlFiles.length === 0) {
19660
19768
  await tryRmdir(legacyDir);
19661
- await tryRmdir(path24.join(vaultPath2, ".claude"));
19769
+ await tryRmdir(path25.join(vaultPath2, ".claude"));
19662
19770
  return;
19663
19771
  }
19664
19772
  await ensurePoliciesDir(vaultPath2);
19665
19773
  const destDir = getPoliciesDir(vaultPath2);
19666
19774
  for (const file of yamlFiles) {
19667
- const src = path24.join(legacyDir, file);
19668
- const dest = path24.join(destDir, file);
19775
+ const src = path25.join(legacyDir, file);
19776
+ const dest = path25.join(destDir, file);
19669
19777
  try {
19670
19778
  await fs22.access(dest);
19671
19779
  } catch {
@@ -19674,7 +19782,7 @@ async function migratePoliciesIfNeeded(vaultPath2) {
19674
19782
  await fs22.unlink(src);
19675
19783
  }
19676
19784
  await tryRmdir(legacyDir);
19677
- await tryRmdir(path24.join(vaultPath2, ".claude"));
19785
+ await tryRmdir(path25.join(vaultPath2, ".claude"));
19678
19786
  }
19679
19787
  async function tryRmdir(dir) {
19680
19788
  try {
@@ -19687,7 +19795,7 @@ async function tryRmdir(dir) {
19687
19795
  }
19688
19796
  var migrationCache = /* @__PURE__ */ new Map();
19689
19797
  async function ensureMigrated(vaultPath2) {
19690
- const key = path24.resolve(vaultPath2);
19798
+ const key = path25.resolve(vaultPath2);
19691
19799
  if (!migrationCache.has(key)) {
19692
19800
  migrationCache.set(key, migratePoliciesIfNeeded(vaultPath2));
19693
19801
  }
@@ -19754,7 +19862,7 @@ async function getPolicyHint(vaultPath2) {
19754
19862
  return "";
19755
19863
  }
19756
19864
  async function ensureFileExists(vaultPath2, notePath) {
19757
- const fullPath = path25.join(vaultPath2, notePath);
19865
+ const fullPath = path26.join(vaultPath2, notePath);
19758
19866
  try {
19759
19867
  await fs23.access(fullPath);
19760
19868
  return null;
@@ -19959,7 +20067,7 @@ async function executeCreateNote(options) {
19959
20067
  if (!pathCheck.valid) {
19960
20068
  return { success: false, result: errorResult(notePath, `Path blocked: ${pathCheck.reason}`), filesWritten: [] };
19961
20069
  }
19962
- const fullPath = path25.join(vaultPath2, notePath);
20070
+ const fullPath = path26.join(vaultPath2, notePath);
19963
20071
  let fileExists = false;
19964
20072
  try {
19965
20073
  await fs23.access(fullPath);
@@ -19969,7 +20077,7 @@ async function executeCreateNote(options) {
19969
20077
  if (fileExists && !overwrite) {
19970
20078
  return { success: false, result: errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`), filesWritten: [] };
19971
20079
  }
19972
- await fs23.mkdir(path25.dirname(fullPath), { recursive: true });
20080
+ await fs23.mkdir(path26.dirname(fullPath), { recursive: true });
19973
20081
  const { maybeApplyWikilinks: maybeApplyWikilinks2 } = await Promise.resolve().then(() => (init_wikilinks(), wikilinks_exports));
19974
20082
  const { content: processedContent } = maybeApplyWikilinks2(content, skipWikilinks ?? false, notePath);
19975
20083
  let finalFrontmatter = frontmatter;
@@ -20003,7 +20111,7 @@ async function executeDeleteNote(options) {
20003
20111
  if (!pathCheck.valid) {
20004
20112
  return { success: false, result: errorResult(notePath, `Path blocked: ${pathCheck.reason}`), filesWritten: [] };
20005
20113
  }
20006
- const fullPath = path25.join(vaultPath2, notePath);
20114
+ const fullPath = path26.join(vaultPath2, notePath);
20007
20115
  try {
20008
20116
  await fs23.access(fullPath);
20009
20117
  } catch {
@@ -20027,10 +20135,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20027
20135
  if (!validation.valid) {
20028
20136
  throw new Error(`Path blocked: ${validation.reason}`);
20029
20137
  }
20030
- const fullPath = path26.join(vaultPath2, notePath);
20031
- await fs24.mkdir(path26.dirname(fullPath), { recursive: true });
20138
+ const fullPath = path27.join(vaultPath2, notePath);
20139
+ await fs24.mkdir(path27.dirname(fullPath), { recursive: true });
20032
20140
  const templates = config2.templates || {};
20033
- const filename = path26.basename(notePath, ".md").toLowerCase();
20141
+ const filename = path27.basename(notePath, ".md").toLowerCase();
20034
20142
  let templatePath;
20035
20143
  let periodicType;
20036
20144
  const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
@@ -20063,7 +20171,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20063
20171
  ];
20064
20172
  for (const candidate of candidates) {
20065
20173
  try {
20066
- await fs24.access(path26.join(vaultPath2, candidate));
20174
+ await fs24.access(path27.join(vaultPath2, candidate));
20067
20175
  templatePath = candidate;
20068
20176
  console.error(`[Flywheel] Template not in config but found at ${candidate} \u2014 using it`);
20069
20177
  break;
@@ -20074,11 +20182,11 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20074
20182
  let templateContent;
20075
20183
  if (templatePath) {
20076
20184
  try {
20077
- const absTemplatePath = path26.join(vaultPath2, templatePath);
20185
+ const absTemplatePath = path27.join(vaultPath2, templatePath);
20078
20186
  templateContent = await fs24.readFile(absTemplatePath, "utf-8");
20079
20187
  } catch {
20080
20188
  console.error(`[Flywheel] Template at ${templatePath} not readable, using minimal fallback`);
20081
- const title = path26.basename(notePath, ".md");
20189
+ const title = path27.basename(notePath, ".md");
20082
20190
  templateContent = `---
20083
20191
  ---
20084
20192
 
@@ -20090,7 +20198,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20090
20198
  if (periodicType) {
20091
20199
  console.error(`[Flywheel] No ${periodicType} template found in config or vault \u2014 using minimal fallback`);
20092
20200
  }
20093
- const title = path26.basename(notePath, ".md");
20201
+ const title = path27.basename(notePath, ".md");
20094
20202
  templateContent = `---
20095
20203
  ---
20096
20204
 
@@ -20099,7 +20207,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20099
20207
  }
20100
20208
  const now = /* @__PURE__ */ new Date();
20101
20209
  const dateStr = now.toISOString().split("T")[0];
20102
- templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path26.basename(notePath, ".md"));
20210
+ templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path27.basename(notePath, ".md"));
20103
20211
  const matter9 = (await import("gray-matter")).default;
20104
20212
  const parsed = matter9(templateContent);
20105
20213
  if (!parsed.data.date) {
@@ -20138,7 +20246,7 @@ function registerMutationTools(server2, getVaultPath, getConfig2 = () => ({})) {
20138
20246
  let noteCreated = false;
20139
20247
  let templateUsed;
20140
20248
  if (create_if_missing && !dry_run) {
20141
- const fullPath = path26.join(vaultPath2, notePath);
20249
+ const fullPath = path27.join(vaultPath2, notePath);
20142
20250
  try {
20143
20251
  await fs24.access(fullPath);
20144
20252
  } catch {
@@ -20149,7 +20257,7 @@ function registerMutationTools(server2, getVaultPath, getConfig2 = () => ({})) {
20149
20257
  }
20150
20258
  }
20151
20259
  if (create_if_missing && dry_run) {
20152
- const fullPath = path26.join(vaultPath2, notePath);
20260
+ const fullPath = path27.join(vaultPath2, notePath);
20153
20261
  try {
20154
20262
  await fs24.access(fullPath);
20155
20263
  } catch {
@@ -20636,7 +20744,7 @@ init_writer();
20636
20744
  init_wikilinks();
20637
20745
  import { z as z15 } from "zod";
20638
20746
  import fs25 from "fs/promises";
20639
- import path27 from "path";
20747
+ import path28 from "path";
20640
20748
  function registerNoteTools(server2, getVaultPath, getIndex) {
20641
20749
  server2.tool(
20642
20750
  "vault_create_note",
@@ -20662,23 +20770,23 @@ function registerNoteTools(server2, getVaultPath, getIndex) {
20662
20770
  if (!validatePath(vaultPath2, notePath)) {
20663
20771
  return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
20664
20772
  }
20665
- const fullPath = path27.join(vaultPath2, notePath);
20773
+ const fullPath = path28.join(vaultPath2, notePath);
20666
20774
  const existsCheck = await ensureFileExists(vaultPath2, notePath);
20667
20775
  if (existsCheck === null && !overwrite) {
20668
20776
  return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
20669
20777
  }
20670
- const dir = path27.dirname(fullPath);
20778
+ const dir = path28.dirname(fullPath);
20671
20779
  await fs25.mkdir(dir, { recursive: true });
20672
20780
  let effectiveContent = content;
20673
20781
  let effectiveFrontmatter = frontmatter;
20674
20782
  if (template) {
20675
- const templatePath = path27.join(vaultPath2, template);
20783
+ const templatePath = path28.join(vaultPath2, template);
20676
20784
  try {
20677
20785
  const raw = await fs25.readFile(templatePath, "utf-8");
20678
20786
  const matter9 = (await import("gray-matter")).default;
20679
20787
  const parsed = matter9(raw);
20680
20788
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
20681
- const title = path27.basename(notePath, ".md");
20789
+ const title = path28.basename(notePath, ".md");
20682
20790
  let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
20683
20791
  if (content) {
20684
20792
  templateContent = templateContent.trimEnd() + "\n\n" + content;
@@ -20697,7 +20805,7 @@ function registerNoteTools(server2, getVaultPath, getIndex) {
20697
20805
  effectiveFrontmatter.created = now.toISOString();
20698
20806
  }
20699
20807
  const warnings = [];
20700
- const noteName = path27.basename(notePath, ".md");
20808
+ const noteName = path28.basename(notePath, ".md");
20701
20809
  const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
20702
20810
  const preflight = await checkPreflightSimilarity(noteName);
20703
20811
  if (preflight.existingEntity) {
@@ -20838,7 +20946,7 @@ ${sources}`;
20838
20946
  }
20839
20947
  return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
20840
20948
  }
20841
- const fullPath = path27.join(vaultPath2, notePath);
20949
+ const fullPath = path28.join(vaultPath2, notePath);
20842
20950
  await fs25.unlink(fullPath);
20843
20951
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
20844
20952
  const message = backlinkWarning ? `Deleted note: ${notePath}
@@ -20860,7 +20968,7 @@ init_git();
20860
20968
  init_wikilinks();
20861
20969
  import { z as z16 } from "zod";
20862
20970
  import fs26 from "fs/promises";
20863
- import path28 from "path";
20971
+ import path29 from "path";
20864
20972
  import matter6 from "gray-matter";
20865
20973
  function escapeRegex(str) {
20866
20974
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -20879,7 +20987,7 @@ function extractWikilinks2(content) {
20879
20987
  return wikilinks;
20880
20988
  }
20881
20989
  function getTitleFromPath(filePath) {
20882
- return path28.basename(filePath, ".md");
20990
+ return path29.basename(filePath, ".md");
20883
20991
  }
20884
20992
  async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
20885
20993
  const results = [];
@@ -20888,7 +20996,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
20888
20996
  const files = [];
20889
20997
  const entries = await fs26.readdir(dir, { withFileTypes: true });
20890
20998
  for (const entry of entries) {
20891
- const fullPath = path28.join(dir, entry.name);
20999
+ const fullPath = path29.join(dir, entry.name);
20892
21000
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
20893
21001
  files.push(...await scanDir(fullPath));
20894
21002
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -20899,7 +21007,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
20899
21007
  }
20900
21008
  const allFiles = await scanDir(vaultPath2);
20901
21009
  for (const filePath of allFiles) {
20902
- const relativePath = path28.relative(vaultPath2, filePath);
21010
+ const relativePath = path29.relative(vaultPath2, filePath);
20903
21011
  const content = await fs26.readFile(filePath, "utf-8");
20904
21012
  const wikilinks = extractWikilinks2(content);
20905
21013
  const matchingLinks = [];
@@ -20986,8 +21094,8 @@ function registerMoveNoteTools(server2, getVaultPath) {
20986
21094
  };
20987
21095
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
20988
21096
  }
20989
- const oldFullPath = path28.join(vaultPath2, oldPath);
20990
- const newFullPath = path28.join(vaultPath2, newPath);
21097
+ const oldFullPath = path29.join(vaultPath2, oldPath);
21098
+ const newFullPath = path29.join(vaultPath2, newPath);
20991
21099
  try {
20992
21100
  await fs26.access(oldFullPath);
20993
21101
  } catch {
@@ -21068,7 +21176,7 @@ function registerMoveNoteTools(server2, getVaultPath) {
21068
21176
  };
21069
21177
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
21070
21178
  }
21071
- const destDir = path28.dirname(newFullPath);
21179
+ const destDir = path29.dirname(newFullPath);
21072
21180
  await fs26.mkdir(destDir, { recursive: true });
21073
21181
  await fs26.rename(oldFullPath, newFullPath);
21074
21182
  let gitCommit;
@@ -21144,10 +21252,10 @@ function registerMoveNoteTools(server2, getVaultPath) {
21144
21252
  if (sanitizedTitle !== newTitle) {
21145
21253
  console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
21146
21254
  }
21147
- const fullPath = path28.join(vaultPath2, notePath);
21148
- const dir = path28.dirname(notePath);
21149
- const newPath = dir === "." ? `${sanitizedTitle}.md` : path28.join(dir, `${sanitizedTitle}.md`);
21150
- const newFullPath = path28.join(vaultPath2, newPath);
21255
+ const fullPath = path29.join(vaultPath2, notePath);
21256
+ const dir = path29.dirname(notePath);
21257
+ const newPath = dir === "." ? `${sanitizedTitle}.md` : path29.join(dir, `${sanitizedTitle}.md`);
21258
+ const newFullPath = path29.join(vaultPath2, newPath);
21151
21259
  try {
21152
21260
  await fs26.access(fullPath);
21153
21261
  } catch {
@@ -21279,7 +21387,7 @@ init_writer();
21279
21387
  init_wikilinks();
21280
21388
  import { z as z17 } from "zod";
21281
21389
  import fs27 from "fs/promises";
21282
- import path29 from "path";
21390
+ import path30 from "path";
21283
21391
  function registerMergeTools(server2, getVaultPath) {
21284
21392
  server2.tool(
21285
21393
  "merge_entities",
@@ -21576,14 +21684,14 @@ async function findSourceNote(vaultPath2, sourceName, excludePath) {
21576
21684
  }
21577
21685
  for (const entry of entries) {
21578
21686
  if (entry.name.startsWith(".")) continue;
21579
- const fullPath = path29.join(dir, entry.name);
21687
+ const fullPath = path30.join(dir, entry.name);
21580
21688
  if (entry.isDirectory()) {
21581
21689
  const found = await scanDir(fullPath);
21582
21690
  if (found) return found;
21583
21691
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
21584
- const basename5 = path29.basename(entry.name, ".md");
21692
+ const basename5 = path30.basename(entry.name, ".md");
21585
21693
  if (basename5.toLowerCase() === targetLower) {
21586
- const relative2 = path29.relative(vaultPath2, fullPath).replace(/\\/g, "/");
21694
+ const relative2 = path30.relative(vaultPath2, fullPath).replace(/\\/g, "/");
21587
21695
  if (relative2 !== excludePath) return relative2;
21588
21696
  }
21589
21697
  }
@@ -21704,7 +21812,7 @@ Message: ${undoResult.undoneCommit.message}` : void 0
21704
21812
  }
21705
21813
 
21706
21814
  // src/tools/write/policy.ts
21707
- import * as path34 from "path";
21815
+ import * as path35 from "path";
21708
21816
  import { z as z20 } from "zod";
21709
21817
 
21710
21818
  // src/core/write/policy/index.ts
@@ -21714,7 +21822,7 @@ init_schema();
21714
21822
  // src/core/write/policy/parser.ts
21715
21823
  init_schema();
21716
21824
  import fs28 from "fs/promises";
21717
- import path30 from "path";
21825
+ import path31 from "path";
21718
21826
  import matter7 from "gray-matter";
21719
21827
  function parseYaml(content) {
21720
21828
  const parsed = matter7(`---
@@ -21765,12 +21873,12 @@ async function loadPolicyFile(filePath) {
21765
21873
  async function loadPolicy(vaultPath2, policyName) {
21766
21874
  await ensureMigrated(vaultPath2);
21767
21875
  const policiesDir = getPoliciesDir(vaultPath2);
21768
- const policyPath = path30.join(policiesDir, `${policyName}.yaml`);
21876
+ const policyPath = path31.join(policiesDir, `${policyName}.yaml`);
21769
21877
  try {
21770
21878
  await fs28.access(policyPath);
21771
21879
  return loadPolicyFile(policyPath);
21772
21880
  } catch {
21773
- const ymlPath = path30.join(policiesDir, `${policyName}.yml`);
21881
+ const ymlPath = path31.join(policiesDir, `${policyName}.yml`);
21774
21882
  try {
21775
21883
  await fs28.access(ymlPath);
21776
21884
  return loadPolicyFile(ymlPath);
@@ -21913,7 +22021,7 @@ init_writer();
21913
22021
  init_git();
21914
22022
  init_wikilinks();
21915
22023
  import fs30 from "fs/promises";
21916
- import path32 from "path";
22024
+ import path33 from "path";
21917
22025
  init_constants2();
21918
22026
  async function executeStep(step, vaultPath2, context, conditionResults, searchFn) {
21919
22027
  const { execute, reason } = shouldStepExecute(step.when, conditionResults);
@@ -22104,12 +22212,12 @@ async function executeCreateNote2(params, vaultPath2, context) {
22104
22212
  let frontmatter = params.frontmatter || {};
22105
22213
  if (params.template) {
22106
22214
  try {
22107
- const templatePath = path32.join(vaultPath2, String(params.template));
22215
+ const templatePath = path33.join(vaultPath2, String(params.template));
22108
22216
  const raw = await fs30.readFile(templatePath, "utf-8");
22109
22217
  const matter9 = (await import("gray-matter")).default;
22110
22218
  const parsed = matter9(raw);
22111
22219
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
22112
- const title = path32.basename(notePath, ".md");
22220
+ const title = path33.basename(notePath, ".md");
22113
22221
  let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
22114
22222
  if (content) {
22115
22223
  templateContent = templateContent.trimEnd() + "\n\n" + content;
@@ -22142,7 +22250,7 @@ async function executeToggleTask(params, vaultPath2) {
22142
22250
  const notePath = String(params.path || "");
22143
22251
  const task = String(params.task || "");
22144
22252
  const section = params.section ? String(params.section) : void 0;
22145
- const fullPath = path32.join(vaultPath2, notePath);
22253
+ const fullPath = path33.join(vaultPath2, notePath);
22146
22254
  try {
22147
22255
  await fs30.access(fullPath);
22148
22256
  } catch {
@@ -22425,7 +22533,7 @@ async function rollbackChanges(vaultPath2, originalContents, filesModified) {
22425
22533
  const pathCheck = await validatePathSecure(vaultPath2, filePath);
22426
22534
  if (!pathCheck.valid) continue;
22427
22535
  const original = originalContents.get(filePath);
22428
- const fullPath = path32.join(vaultPath2, filePath);
22536
+ const fullPath = path33.join(vaultPath2, filePath);
22429
22537
  if (original === null) {
22430
22538
  try {
22431
22539
  await fs30.unlink(fullPath);
@@ -22480,7 +22588,7 @@ async function previewPolicy(policy, vaultPath2, variables) {
22480
22588
 
22481
22589
  // src/core/write/policy/storage.ts
22482
22590
  import fs31 from "fs/promises";
22483
- import path33 from "path";
22591
+ import path34 from "path";
22484
22592
  async function listPolicies(vaultPath2) {
22485
22593
  await ensureMigrated(vaultPath2);
22486
22594
  const dir = getPoliciesDir(vaultPath2);
@@ -22491,7 +22599,7 @@ async function listPolicies(vaultPath2) {
22491
22599
  if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
22492
22600
  continue;
22493
22601
  }
22494
- const filePath = path33.join(dir, file);
22602
+ const filePath = path34.join(dir, file);
22495
22603
  const stat4 = await fs31.stat(filePath);
22496
22604
  const content = await fs31.readFile(filePath, "utf-8");
22497
22605
  const metadata = extractPolicyMetadata(content);
@@ -22517,7 +22625,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
22517
22625
  const dir = getPoliciesDir(vaultPath2);
22518
22626
  await ensurePoliciesDir(vaultPath2);
22519
22627
  const filename = `${policyName}.yaml`;
22520
- const filePath = path33.join(dir, filename);
22628
+ const filePath = path34.join(dir, filename);
22521
22629
  if (!overwrite) {
22522
22630
  try {
22523
22631
  await fs31.access(filePath);
@@ -22631,7 +22739,7 @@ function registerPolicyTools(server2, getVaultPath, getSearchFn) {
22631
22739
  const policies = await listPolicies(vaultPath2);
22632
22740
  const response = {
22633
22741
  success: true,
22634
- vault: path34.basename(vaultPath2),
22742
+ vault: path35.basename(vaultPath2),
22635
22743
  vault_path: vaultPath2,
22636
22744
  count: policies.length,
22637
22745
  policies: policies.map((p) => ({
@@ -23065,7 +23173,7 @@ import { z as z21 } from "zod";
23065
23173
 
23066
23174
  // src/core/write/tagRename.ts
23067
23175
  import * as fs32 from "fs/promises";
23068
- import * as path35 from "path";
23176
+ import * as path36 from "path";
23069
23177
  import matter8 from "gray-matter";
23070
23178
  import { getProtectedZones as getProtectedZones2 } from "@velvetmonkey/vault-core";
23071
23179
  function getNotesInFolder3(index, folder) {
@@ -23171,7 +23279,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
23171
23279
  const previews = [];
23172
23280
  let totalChanges = 0;
23173
23281
  for (const note of affectedNotes) {
23174
- const fullPath = path35.join(vaultPath2, note.path);
23282
+ const fullPath = path36.join(vaultPath2, note.path);
23175
23283
  let fileContent;
23176
23284
  try {
23177
23285
  fileContent = await fs32.readFile(fullPath, "utf-8");
@@ -24301,7 +24409,7 @@ init_wikilinks();
24301
24409
  init_wikilinkFeedback();
24302
24410
  import { z as z28 } from "zod";
24303
24411
  import * as fs33 from "fs/promises";
24304
- import * as path36 from "path";
24412
+ import * as path37 from "path";
24305
24413
  import { scanVaultEntities as scanVaultEntities4, SCHEMA_VERSION as SCHEMA_VERSION2 } from "@velvetmonkey/vault-core";
24306
24414
  init_embeddings();
24307
24415
  function hasSkipWikilinks(content) {
@@ -24317,13 +24425,13 @@ async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
24317
24425
  const entries = await fs33.readdir(dirPath, { withFileTypes: true });
24318
24426
  for (const entry of entries) {
24319
24427
  if (entry.name.startsWith(".")) continue;
24320
- const fullPath = path36.join(dirPath, entry.name);
24428
+ const fullPath = path37.join(dirPath, entry.name);
24321
24429
  if (entry.isDirectory()) {
24322
24430
  if (excludeFolders.some((f) => entry.name.toLowerCase() === f.toLowerCase())) continue;
24323
24431
  const sub = await collectMarkdownFiles(fullPath, basePath, excludeFolders);
24324
24432
  results.push(...sub);
24325
24433
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
24326
- results.push(path36.relative(basePath, fullPath));
24434
+ results.push(path37.relative(basePath, fullPath));
24327
24435
  }
24328
24436
  }
24329
24437
  } catch {
@@ -24353,7 +24461,7 @@ var EXCLUDE_FOLDERS = [
24353
24461
  ];
24354
24462
  function buildStatusReport(stateDb2, vaultPath2) {
24355
24463
  const recommendations = [];
24356
- const dbPath = path36.join(vaultPath2, ".flywheel", "state.db");
24464
+ const dbPath = path37.join(vaultPath2, ".flywheel", "state.db");
24357
24465
  const statedbExists = stateDb2 !== null;
24358
24466
  if (!statedbExists) {
24359
24467
  recommendations.push("StateDb not initialized \u2014 server needs restart");
@@ -24480,7 +24588,7 @@ async function executeRun(stateDb2, vaultPath2) {
24480
24588
  const allFiles = await collectMarkdownFiles(vaultPath2, vaultPath2, EXCLUDE_FOLDERS);
24481
24589
  let eligible = 0;
24482
24590
  for (const relativePath of allFiles) {
24483
- const fullPath = path36.join(vaultPath2, relativePath);
24591
+ const fullPath = path37.join(vaultPath2, relativePath);
24484
24592
  let content;
24485
24593
  try {
24486
24594
  content = await fs33.readFile(fullPath, "utf-8");
@@ -24538,7 +24646,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
24538
24646
  const eligible = [];
24539
24647
  let notesSkipped = 0;
24540
24648
  for (const relativePath of allFiles) {
24541
- const fullPath = path36.join(vaultPath2, relativePath);
24649
+ const fullPath = path37.join(vaultPath2, relativePath);
24542
24650
  let content;
24543
24651
  try {
24544
24652
  content = await fs33.readFile(fullPath, "utf-8");
@@ -24568,7 +24676,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
24568
24676
  match_count: result.linksAdded
24569
24677
  });
24570
24678
  if (!dryRun) {
24571
- const fullPath = path36.join(vaultPath2, relativePath);
24679
+ const fullPath = path37.join(vaultPath2, relativePath);
24572
24680
  await fs33.writeFile(fullPath, result.content, "utf-8");
24573
24681
  notesModified++;
24574
24682
  if (stateDb2) {
@@ -24747,7 +24855,7 @@ import { z as z30 } from "zod";
24747
24855
 
24748
24856
  // src/core/read/similarity.ts
24749
24857
  import * as fs34 from "fs";
24750
- import * as path37 from "path";
24858
+ import * as path38 from "path";
24751
24859
  init_embeddings();
24752
24860
 
24753
24861
  // src/core/read/mmr.ts
@@ -24817,7 +24925,7 @@ function extractKeyTerms(content, maxTerms = 15) {
24817
24925
  }
24818
24926
  function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
24819
24927
  const limit = options.limit ?? 10;
24820
- const absPath = path37.join(vaultPath2, sourcePath);
24928
+ const absPath = path38.join(vaultPath2, sourcePath);
24821
24929
  let content;
24822
24930
  try {
24823
24931
  content = fs34.readFileSync(absPath, "utf-8");
@@ -24959,7 +25067,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb4) {
24959
25067
  diversity: z30.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
24960
25068
  }
24961
25069
  },
24962
- async ({ path: path40, limit, diversity }) => {
25070
+ async ({ path: path41, limit, diversity }) => {
24963
25071
  const index = getIndex();
24964
25072
  const vaultPath2 = getVaultPath();
24965
25073
  const stateDb2 = getStateDb4();
@@ -24968,10 +25076,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb4) {
24968
25076
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
24969
25077
  };
24970
25078
  }
24971
- if (!index.notes.has(path40)) {
25079
+ if (!index.notes.has(path41)) {
24972
25080
  return {
24973
25081
  content: [{ type: "text", text: JSON.stringify({
24974
- error: `Note not found: ${path40}`,
25082
+ error: `Note not found: ${path41}`,
24975
25083
  hint: "Use the full relative path including .md extension"
24976
25084
  }, null, 2) }]
24977
25085
  };
@@ -24983,12 +25091,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb4) {
24983
25091
  };
24984
25092
  const useHybrid = hasEmbeddingsIndex();
24985
25093
  const method = useHybrid ? "hybrid" : "bm25";
24986
- const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path40, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path40, opts);
25094
+ const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path41, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path41, opts);
24987
25095
  return {
24988
25096
  content: [{
24989
25097
  type: "text",
24990
25098
  text: JSON.stringify({
24991
- source: path40,
25099
+ source: path41,
24992
25100
  method,
24993
25101
  count: results.length,
24994
25102
  similar: results
@@ -26862,9 +26970,9 @@ function registerVaultResources(server2, getIndex) {
26862
26970
  }
26863
26971
 
26864
26972
  // src/tool-registry.ts
26865
- var __trFilename = fileURLToPath2(import.meta.url);
26866
- var __trDirname = dirname5(__trFilename);
26867
- var trPkg = JSON.parse(readFileSync5(join18(__trDirname, "../package.json"), "utf-8"));
26973
+ var __trFilename = fileURLToPath3(import.meta.url);
26974
+ var __trDirname = dirname6(__trFilename);
26975
+ var trPkg = JSON.parse(readFileSync5(join19(__trDirname, "../package.json"), "utf-8"));
26868
26976
  var ACTIVATION_PATTERNS = [
26869
26977
  {
26870
26978
  category: "graph",
@@ -26902,6 +27010,34 @@ var ACTIVATION_PATTERNS = [
26902
27010
  patterns: [/\b(delete note|move note|rename note|merge entities|merge notes?)\b/i]
26903
27011
  }
26904
27012
  ];
27013
+ var MUTATING_TOOL_NAMES = /* @__PURE__ */ new Set([
27014
+ "vault_add_to_section",
27015
+ "vault_remove_from_section",
27016
+ "vault_replace_in_section",
27017
+ "vault_add_task",
27018
+ "vault_toggle_task",
27019
+ "vault_update_frontmatter",
27020
+ "vault_create_note",
27021
+ "vault_delete_note",
27022
+ "vault_move_note",
27023
+ "vault_rename_note",
27024
+ "merge_entities",
27025
+ "absorb_as_alias",
27026
+ "vault_undo_last_mutation",
27027
+ "policy",
27028
+ "rename_tag",
27029
+ "wikilink_feedback",
27030
+ "tool_selection_feedback",
27031
+ "vault_record_correction",
27032
+ "vault_resolve_correction",
27033
+ "memory",
27034
+ "flywheel_config",
27035
+ "vault_init",
27036
+ "rename_field",
27037
+ "migrate_field_values",
27038
+ "refresh_index",
27039
+ "init_semantic"
27040
+ ]);
26905
27041
  function getPatternSignals(raw) {
26906
27042
  if (!raw) return [];
26907
27043
  return ACTIVATION_PATTERNS.filter(({ patterns }) => patterns.some((pattern) => pattern.test(raw))).map(({ category, tier }) => ({ category, tier }));
@@ -27094,7 +27230,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27094
27230
  let totalBytes = 0;
27095
27231
  for (const p of notePaths) {
27096
27232
  try {
27097
- totalBytes += statSync6(path38.join(vp, p)).size;
27233
+ totalBytes += statSync6(path39.join(vp, p)).size;
27098
27234
  } catch {
27099
27235
  }
27100
27236
  }
@@ -27127,6 +27263,26 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27127
27263
  };
27128
27264
  }
27129
27265
  const isMultiVault = registry?.isMultiVault ?? false;
27266
+ function getTargetVaultContext(params) {
27267
+ if (!registry) return null;
27268
+ if (isMultiVault) {
27269
+ const vaultName = typeof params?.vault === "string" ? params.vault : void 0;
27270
+ return registry.getContext(vaultName);
27271
+ }
27272
+ return registry.getContext();
27273
+ }
27274
+ function wrapWithIntegrityGate(toolName, handler) {
27275
+ if (!MUTATING_TOOL_NAMES.has(toolName)) return handler;
27276
+ return async (...args) => {
27277
+ const params = args[0] && typeof args[0] === "object" ? args[0] : void 0;
27278
+ const vaultCtx = getTargetVaultContext(params);
27279
+ const integrityState = vaultCtx?.integrityState ?? getActiveScopeOrNull()?.integrityState;
27280
+ if (integrityState === "failed") {
27281
+ throw new Error("StateDb integrity failed; write operations are disabled until recovery/restart.");
27282
+ }
27283
+ return handler(...args);
27284
+ };
27285
+ }
27130
27286
  function wrapWithVaultActivation(toolName, handler) {
27131
27287
  if (!isMultiVault || !registry || !vaultCallbacks) return handler;
27132
27288
  return async (...args) => {
@@ -27242,6 +27398,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27242
27398
  if (args.length > 0 && typeof args[args.length - 1] === "function") {
27243
27399
  let handler = args[args.length - 1];
27244
27400
  handler = wrapWithVaultActivation(name, handler);
27401
+ handler = wrapWithIntegrityGate(name, handler);
27245
27402
  args[args.length - 1] = wrapWithTracking(name, handler);
27246
27403
  }
27247
27404
  const registered = origTool(name, ...args);
@@ -27256,6 +27413,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27256
27413
  if (args.length > 0 && typeof args[args.length - 1] === "function") {
27257
27414
  let handler = args[args.length - 1];
27258
27415
  handler = wrapWithVaultActivation(name, handler);
27416
+ handler = wrapWithIntegrityGate(name, handler);
27259
27417
  args[args.length - 1] = wrapWithTracking(name, handler);
27260
27418
  }
27261
27419
  const registered = origRegisterTool(name, ...args);
@@ -27349,7 +27507,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27349
27507
  }
27350
27508
  function registerAllTools(targetServer, ctx, controller) {
27351
27509
  const { getVaultPath: gvp, getVaultIndex: gvi, getStateDb: gsd, getFlywheelConfig: gcf } = ctx;
27352
- registerHealthTools(targetServer, gvi, gvp, gcf, gsd, ctx.getWatcherStatus, () => trPkg.version, ctx.getPipelineActivity);
27510
+ registerHealthTools(targetServer, gvi, gvp, gcf, gsd, ctx.getWatcherStatus, () => trPkg.version, ctx.getPipelineActivity, ctx.getVaultRuntimeState);
27353
27511
  registerSystemTools(
27354
27512
  targetServer,
27355
27513
  gvi,
@@ -27445,9 +27603,9 @@ function registerAllTools(targetServer, ctx, controller) {
27445
27603
  }
27446
27604
 
27447
27605
  // src/index.ts
27448
- var __filename2 = fileURLToPath3(import.meta.url);
27449
- var __dirname = dirname7(__filename2);
27450
- var pkg = JSON.parse(readFileSync6(join20(__dirname, "../package.json"), "utf-8"));
27606
+ var __filename2 = fileURLToPath4(import.meta.url);
27607
+ var __dirname = dirname8(__filename2);
27608
+ var pkg = JSON.parse(readFileSync6(join21(__dirname, "../package.json"), "utf-8"));
27451
27609
  var vaultPath;
27452
27610
  var resolvedVaultPath;
27453
27611
  var vaultIndex;
@@ -27463,6 +27621,7 @@ var lastMcpRequestAt = 0;
27463
27621
  var lastFullRebuildAt = 0;
27464
27622
  var startupScanFiles = null;
27465
27623
  var deferredScheduler = null;
27624
+ var integrityRuns = /* @__PURE__ */ new Map();
27466
27625
  function getWatcherStatus() {
27467
27626
  if (vaultRegistry) {
27468
27627
  const name = globalThis.__flywheel_active_vault;
@@ -27507,6 +27666,20 @@ function buildRegistryContext() {
27507
27666
  getFlywheelConfig: () => getActiveScopeOrNull()?.flywheelConfig ?? flywheelConfig,
27508
27667
  getWatcherStatus,
27509
27668
  getPipelineActivity: () => getActiveScopeOrNull()?.pipelineActivity ?? null,
27669
+ getVaultRuntimeState: () => {
27670
+ const scope = getActiveScopeOrNull();
27671
+ return {
27672
+ bootState: scope?.bootState ?? "booting",
27673
+ integrityState: scope?.integrityState ?? "unknown",
27674
+ integrityCheckInProgress: scope?.integrityCheckInProgress ?? false,
27675
+ integrityStartedAt: scope?.integrityStartedAt ?? null,
27676
+ integritySource: scope?.integritySource ?? null,
27677
+ lastIntegrityCheckedAt: scope?.lastIntegrityCheckedAt ?? null,
27678
+ lastIntegrityDurationMs: scope?.lastIntegrityDurationMs ?? null,
27679
+ lastIntegrityDetail: scope?.lastIntegrityDetail ?? null,
27680
+ lastBackupAt: scope?.lastBackupAt ?? null
27681
+ };
27682
+ },
27510
27683
  updateVaultIndex,
27511
27684
  updateFlywheelConfig
27512
27685
  };
@@ -27608,6 +27781,110 @@ function loadVaultCooccurrence(ctx) {
27608
27781
  serverLog("index", `[${ctx.name}] Co-occurrence: loaded from cache (${Object.keys(cachedCooc.index.associations).length} entities, ${cachedCooc.index._metadata.total_associations} associations)`);
27609
27782
  }
27610
27783
  }
27784
+ function hydrateIntegrityMetadata(ctx) {
27785
+ if (!ctx.stateDb) return;
27786
+ const checkedAtRow = ctx.stateDb.getMetadataValue.get(INTEGRITY_METADATA_KEYS.checkedAt);
27787
+ const statusRow = ctx.stateDb.getMetadataValue.get(INTEGRITY_METADATA_KEYS.status);
27788
+ const durationRow = ctx.stateDb.getMetadataValue.get(INTEGRITY_METADATA_KEYS.durationMs);
27789
+ const detailRow = ctx.stateDb.getMetadataValue.get(INTEGRITY_METADATA_KEYS.detail);
27790
+ ctx.lastIntegrityCheckedAt = checkedAtRow ? parseInt(checkedAtRow.value, 10) || null : null;
27791
+ ctx.lastIntegrityDurationMs = durationRow ? parseInt(durationRow.value, 10) || null : null;
27792
+ ctx.lastIntegrityDetail = detailRow?.value ? detailRow.value : null;
27793
+ const status = statusRow?.value;
27794
+ if (status === "healthy" || status === "failed" || status === "error") {
27795
+ ctx.integrityState = status;
27796
+ }
27797
+ }
27798
+ function setBootState(ctx, state2) {
27799
+ ctx.bootState = state2;
27800
+ if (globalThis.__flywheel_active_vault === ctx.name) {
27801
+ setActiveScope(buildVaultScope(ctx));
27802
+ }
27803
+ }
27804
+ function setIntegrityState(ctx, state2, detail = ctx.lastIntegrityDetail, durationMs = ctx.lastIntegrityDurationMs) {
27805
+ ctx.integrityState = state2;
27806
+ ctx.lastIntegrityDetail = detail;
27807
+ ctx.lastIntegrityDurationMs = durationMs;
27808
+ if (state2 === "failed") {
27809
+ ctx.bootState = "degraded";
27810
+ }
27811
+ if (globalThis.__flywheel_active_vault === ctx.name) {
27812
+ setActiveScope(buildVaultScope(ctx));
27813
+ }
27814
+ }
27815
+ function persistIntegrityMetadata(ctx) {
27816
+ if (!ctx.stateDb || ctx.lastIntegrityCheckedAt == null) return;
27817
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.checkedAt, String(ctx.lastIntegrityCheckedAt));
27818
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.status, ctx.integrityState);
27819
+ if (ctx.lastIntegrityDurationMs != null) {
27820
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.durationMs, String(ctx.lastIntegrityDurationMs));
27821
+ }
27822
+ if (ctx.lastIntegrityDetail) {
27823
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.detail, ctx.lastIntegrityDetail);
27824
+ } else {
27825
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.detail, "");
27826
+ }
27827
+ }
27828
+ function shouldRunBackup(ctx) {
27829
+ if (ctx.lastBackupAt == null) return true;
27830
+ return Date.now() - ctx.lastBackupAt >= INTEGRITY_BACKUP_INTERVAL_MS;
27831
+ }
27832
+ async function runIntegrityCheck(ctx, source, options = {}) {
27833
+ if (!ctx.stateDb) {
27834
+ return { status: "error", detail: "StateDb not available", durationMs: 0, backupCreated: false };
27835
+ }
27836
+ if (!options.force && ctx.integrityState === "healthy" && ctx.lastIntegrityCheckedAt != null) {
27837
+ if (Date.now() - ctx.lastIntegrityCheckedAt < INTEGRITY_CHECK_INTERVAL_MS) {
27838
+ return {
27839
+ status: "healthy",
27840
+ detail: ctx.lastIntegrityDetail,
27841
+ durationMs: ctx.lastIntegrityDurationMs ?? 0,
27842
+ backupCreated: false
27843
+ };
27844
+ }
27845
+ }
27846
+ const existing = integrityRuns.get(ctx.name);
27847
+ if (existing) return existing;
27848
+ ctx.integrityCheckInProgress = true;
27849
+ ctx.integrityStartedAt = Date.now();
27850
+ ctx.integritySource = source;
27851
+ setIntegrityState(ctx, "checking", ctx.lastIntegrityDetail, ctx.lastIntegrityDurationMs);
27852
+ serverLog("statedb", `[${ctx.name}] Integrity check started (${source})`);
27853
+ const promise = runIntegrityWorker({
27854
+ dbPath: ctx.stateDb.dbPath,
27855
+ runBackup: shouldRunBackup(ctx),
27856
+ busyTimeoutMs: 5e3
27857
+ }).then((result) => {
27858
+ ctx.integrityCheckInProgress = false;
27859
+ ctx.integrityStartedAt = null;
27860
+ ctx.integritySource = source;
27861
+ ctx.lastIntegrityCheckedAt = Date.now();
27862
+ ctx.lastIntegrityDurationMs = result.durationMs;
27863
+ ctx.lastIntegrityDetail = result.detail;
27864
+ if (result.backupCreated) {
27865
+ ctx.lastBackupAt = Date.now();
27866
+ }
27867
+ if (result.status === "healthy") {
27868
+ setIntegrityState(ctx, "healthy", result.detail, result.durationMs);
27869
+ serverLog("statedb", `[${ctx.name}] Integrity check passed in ${result.durationMs}ms`);
27870
+ } else if (result.status === "failed") {
27871
+ setIntegrityState(ctx, "failed", result.detail, result.durationMs);
27872
+ serverLog("statedb", `[${ctx.name}] Integrity check failed: ${result.detail}`, "error");
27873
+ } else {
27874
+ setIntegrityState(ctx, "error", result.detail, result.durationMs);
27875
+ serverLog("statedb", `[${ctx.name}] Integrity check error: ${result.detail}`, "warn");
27876
+ }
27877
+ persistIntegrityMetadata(ctx);
27878
+ return result;
27879
+ }).finally(() => {
27880
+ integrityRuns.delete(ctx.name);
27881
+ if (globalThis.__flywheel_active_vault === ctx.name) {
27882
+ setActiveScope(buildVaultScope(ctx));
27883
+ }
27884
+ });
27885
+ integrityRuns.set(ctx.name, promise);
27886
+ return promise;
27887
+ }
27611
27888
  async function initializeVault(name, vaultPathArg) {
27612
27889
  const ctx = {
27613
27890
  name,
@@ -27625,11 +27902,21 @@ async function initializeVault(name, vaultPathArg) {
27625
27902
  lastEntityScanAt: 0,
27626
27903
  lastHubScoreRebuildAt: 0,
27627
27904
  lastIndexCacheSaveAt: 0,
27628
- pipelineActivity: createEmptyPipelineActivity()
27905
+ pipelineActivity: createEmptyPipelineActivity(),
27906
+ bootState: "booting",
27907
+ integrityState: "unknown",
27908
+ integrityCheckInProgress: false,
27909
+ integrityStartedAt: null,
27910
+ integritySource: null,
27911
+ lastIntegrityCheckedAt: null,
27912
+ lastIntegrityDurationMs: null,
27913
+ lastIntegrityDetail: null,
27914
+ lastBackupAt: null
27629
27915
  };
27630
27916
  try {
27631
27917
  ctx.stateDb = openStateDb(vaultPathArg);
27632
27918
  serverLog("statedb", `[${name}] StateDb initialized`);
27919
+ hydrateIntegrityMetadata(ctx);
27633
27920
  const vaultInitRow = ctx.stateDb.getMetadataValue.get("vault_init_last_run_at");
27634
27921
  if (!vaultInitRow) {
27635
27922
  serverLog("server", `[${name}] Vault not initialized \u2014 call vault_init to enrich legacy notes`);
@@ -27653,7 +27940,16 @@ function buildVaultScope(ctx) {
27653
27940
  indexError: ctx.indexError,
27654
27941
  embeddingsBuilding: ctx.embeddingsBuilding,
27655
27942
  entityEmbeddingsMap: getEntityEmbeddingsMap(),
27656
- pipelineActivity: ctx.pipelineActivity
27943
+ pipelineActivity: ctx.pipelineActivity,
27944
+ bootState: ctx.bootState,
27945
+ integrityState: ctx.integrityState,
27946
+ integrityCheckInProgress: ctx.integrityCheckInProgress,
27947
+ integrityStartedAt: ctx.integrityStartedAt,
27948
+ integritySource: ctx.integritySource,
27949
+ lastIntegrityCheckedAt: ctx.lastIntegrityCheckedAt,
27950
+ lastIntegrityDurationMs: ctx.lastIntegrityDurationMs,
27951
+ lastIntegrityDetail: ctx.lastIntegrityDetail,
27952
+ lastBackupAt: ctx.lastBackupAt
27657
27953
  };
27658
27954
  }
27659
27955
  function activateVault(ctx, skipEmbeddingLoad = false) {
@@ -27807,6 +28103,9 @@ async function bootVault(ctx, startTime) {
27807
28103
  serverLog("index", `[${ctx.name}] Failed to build vault index: ${err instanceof Error ? err.message : err}`, "error");
27808
28104
  }
27809
28105
  }
28106
+ if (ctx.bootState !== "degraded") {
28107
+ setBootState(ctx, "ready");
28108
+ }
27810
28109
  }
27811
28110
  async function main() {
27812
28111
  const vaultConfigs = parseVaultConfig();
@@ -27816,7 +28115,7 @@ async function main() {
27816
28115
  } catch {
27817
28116
  resolvedVaultPath = vaultPath.replace(/\\/g, "/");
27818
28117
  }
27819
- if (!existsSync3(resolvedVaultPath)) {
28118
+ if (!existsSync4(resolvedVaultPath)) {
27820
28119
  console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
27821
28120
  console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
27822
28121
  process.exit(1);
@@ -27831,14 +28130,17 @@ async function main() {
27831
28130
  vaultRegistry.addContext(primaryCtx2);
27832
28131
  stateDb = primaryCtx2.stateDb;
27833
28132
  activateVault(primaryCtx2, true);
28133
+ serverLog("server", `[${primaryCtx2.name}] stateDb_open=${Date.now() - startTime}ms`);
27834
28134
  } else {
27835
28135
  vaultRegistry = new VaultRegistry("default");
27836
28136
  const ctx = await initializeVault("default", vaultPath);
27837
28137
  vaultRegistry.addContext(ctx);
27838
28138
  stateDb = ctx.stateDb;
27839
28139
  activateVault(ctx, true);
28140
+ serverLog("server", `[${ctx.name}] stateDb_open=${Date.now() - startTime}ms`);
27840
28141
  }
27841
28142
  await initToolRouting();
28143
+ serverLog("server", `tool_routing=${Date.now() - startTime}ms`);
27842
28144
  if (stateDb) {
27843
28145
  try {
27844
28146
  const vaultName = vaultRegistry?.primaryName ?? "default";
@@ -27930,29 +28232,16 @@ async function main() {
27930
28232
  });
27931
28233
  }
27932
28234
  const primaryCtx = vaultRegistry.getContext();
27933
- if (primaryCtx.stateDb) {
27934
- const integrity = checkDbIntegrity2(primaryCtx.stateDb.db);
27935
- if (integrity.ok) {
27936
- safeBackupAsync2(primaryCtx.stateDb.db, primaryCtx.stateDb.dbPath).catch((err) => {
27937
- serverLog("backup", `[${primaryCtx.name}] Safe backup failed: ${err}`, "error");
27938
- });
27939
- } else {
27940
- serverLog("statedb", `[${primaryCtx.name}] Integrity check failed: ${integrity.detail} \u2014 recreating`, "error");
27941
- const dbPath = primaryCtx.stateDb.dbPath;
27942
- preserveCorruptedDb(dbPath);
27943
- primaryCtx.stateDb.close();
27944
- deleteStateDbFiles(dbPath);
27945
- primaryCtx.stateDb = openStateDb(primaryCtx.vaultPath);
27946
- attemptSalvage(primaryCtx.stateDb.db, dbPath);
27947
- stateDb = primaryCtx.stateDb;
27948
- activateVault(primaryCtx, true);
27949
- }
27950
- serverLog("statedb", `[${primaryCtx.name}] Integrity check passed in ${Date.now() - startTime}ms`);
27951
- }
28235
+ setBootState(primaryCtx, "transport_connected");
28236
+ serverLog("server", `[${primaryCtx.name}] transport_connect=${Date.now() - startTime}ms`);
28237
+ serverLog("server", `[${primaryCtx.name}] integrity_check_started=${Date.now() - startTime}ms`);
28238
+ void runIntegrityCheck(primaryCtx, "startup");
28239
+ setBootState(primaryCtx, "booting");
27952
28240
  loadVaultCooccurrence(primaryCtx);
27953
28241
  activateVault(primaryCtx);
27954
28242
  await bootVault(primaryCtx, startTime);
27955
28243
  activateVault(primaryCtx);
28244
+ serverLog("server", `[${primaryCtx.name}] boot_complete=${Date.now() - startTime}ms`);
27956
28245
  serverReady = true;
27957
28246
  const watchdogInterval = parseInt(process.env.FLYWHEEL_WATCHDOG_INTERVAL ?? "0", 10);
27958
28247
  if (watchdogInterval > 0 && (transportMode === "http" || transportMode === "both")) {
@@ -28014,6 +28303,9 @@ async function main() {
28014
28303
  const ctx = await initializeVault(vc.name, vc.path);
28015
28304
  vaultRegistry.addContext(ctx);
28016
28305
  invalidateHttpPool();
28306
+ setBootState(ctx, "transport_connected");
28307
+ void runIntegrityCheck(ctx, "startup");
28308
+ setBootState(ctx, "booting");
28017
28309
  loadVaultCooccurrence(ctx);
28018
28310
  activateVault(ctx);
28019
28311
  await bootVault(ctx, startTime);
@@ -28229,7 +28521,7 @@ async function runPostIndexWork(ctx) {
28229
28521
  if (attempt < MAX_BUILD_RETRIES) {
28230
28522
  const delay = 1e4;
28231
28523
  serverLog("semantic", `Build failed (attempt ${attempt}/${MAX_BUILD_RETRIES}): ${msg}. Retrying in ${delay / 1e3}s...`, "error");
28232
- await new Promise((resolve3) => setTimeout(resolve3, delay));
28524
+ await new Promise((resolve4) => setTimeout(resolve4, delay));
28233
28525
  return attemptBuild(attempt + 1);
28234
28526
  }
28235
28527
  serverLog("semantic", `Embeddings build failed after ${MAX_BUILD_RETRIES} attempts: ${msg}`, "error");
@@ -28285,8 +28577,8 @@ async function runPostIndexWork(ctx) {
28285
28577
  }
28286
28578
  } catch {
28287
28579
  try {
28288
- const dir = path39.dirname(rawPath);
28289
- const base = path39.basename(rawPath);
28580
+ const dir = path40.dirname(rawPath);
28581
+ const base = path40.basename(rawPath);
28290
28582
  const resolvedDir = realpathSync(dir).replace(/\\/g, "/");
28291
28583
  for (const prefix of vaultPrefixes) {
28292
28584
  if (resolvedDir.startsWith(prefix + "/") || resolvedDir === prefix) {
@@ -28318,7 +28610,7 @@ async function runPostIndexWork(ctx) {
28318
28610
  continue;
28319
28611
  }
28320
28612
  try {
28321
- const content = await fs35.readFile(path39.join(vp, event.path), "utf-8");
28613
+ const content = await fs35.readFile(path40.join(vp, event.path), "utf-8");
28322
28614
  const hash = createHash4("sha256").update(content).digest("hex").slice(0, 16);
28323
28615
  if (lastContentHashes.get(event.path) === hash) {
28324
28616
  serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
@@ -28405,7 +28697,8 @@ async function runPostIndexWork(ctx) {
28405
28697
  updateEntitiesInStateDb,
28406
28698
  getVaultIndex: () => vaultIndex,
28407
28699
  buildVaultIndex,
28408
- deferredScheduler: deferredScheduler ?? void 0
28700
+ deferredScheduler: deferredScheduler ?? void 0,
28701
+ runIntegrityCheck
28409
28702
  });
28410
28703
  await runner.run();
28411
28704
  };