@velvetmonkey/flywheel-memory 2.5.11 → 2.5.13

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);
@@ -5185,16 +5185,16 @@ var init_tool_embeddings_generated = __esm({
5185
5185
 
5186
5186
  // src/core/write/path-security.ts
5187
5187
  import fs20 from "fs/promises";
5188
- import path22 from "path";
5188
+ import path23 from "path";
5189
5189
  function isSensitivePath(filePath) {
5190
5190
  const normalizedPath = filePath.replace(/\\/g, "/");
5191
5191
  return SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath));
5192
5192
  }
5193
5193
  function isWithinDirectory(child, parent, allowEqual = false) {
5194
- const rel = path22.relative(path22.resolve(parent), path22.resolve(child));
5194
+ const rel = path23.relative(path23.resolve(parent), path23.resolve(child));
5195
5195
  if (rel === "") return allowEqual;
5196
- const firstSeg = rel.split(path22.sep)[0];
5197
- return firstSeg !== ".." && !path22.isAbsolute(rel);
5196
+ const firstSeg = rel.split(path23.sep)[0];
5197
+ return firstSeg !== ".." && !path23.isAbsolute(rel);
5198
5198
  }
5199
5199
  function validatePath(vaultPath2, notePath) {
5200
5200
  if (notePath.startsWith("/")) {
@@ -5206,11 +5206,11 @@ function validatePath(vaultPath2, notePath) {
5206
5206
  if (notePath.startsWith("\\")) {
5207
5207
  return false;
5208
5208
  }
5209
- return isWithinDirectory(path22.resolve(vaultPath2, notePath), vaultPath2);
5209
+ return isWithinDirectory(path23.resolve(vaultPath2, notePath), vaultPath2);
5210
5210
  }
5211
5211
  function sanitizeNotePath(notePath) {
5212
- const dir = path22.dirname(notePath);
5213
- let filename = path22.basename(notePath);
5212
+ const dir = path23.dirname(notePath);
5213
+ let filename = path23.basename(notePath);
5214
5214
  const ext = filename.endsWith(".md") ? ".md" : "";
5215
5215
  let stem2 = ext ? filename.slice(0, -ext.length) : filename;
5216
5216
  stem2 = stem2.replace(/\s+/g, "-");
@@ -5219,7 +5219,7 @@ function sanitizeNotePath(notePath) {
5219
5219
  stem2 = stem2.replace(/-{2,}/g, "-");
5220
5220
  stem2 = stem2.replace(/^-+|-+$/g, "");
5221
5221
  filename = stem2 + (ext || ".md");
5222
- return dir === "." ? filename : path22.join(dir, filename).replace(/\\/g, "/");
5222
+ return dir === "." ? filename : path23.join(dir, filename).replace(/\\/g, "/");
5223
5223
  }
5224
5224
  async function validatePathSecure(vaultPath2, notePath) {
5225
5225
  if (notePath.startsWith("/")) {
@@ -5240,14 +5240,14 @@ async function validatePathSecure(vaultPath2, notePath) {
5240
5240
  reason: "Absolute paths not allowed"
5241
5241
  };
5242
5242
  }
5243
- const firstSeg = path22.normalize(notePath).split(path22.sep).filter(Boolean)[0];
5243
+ const firstSeg = path23.normalize(notePath).split(path23.sep).filter(Boolean)[0];
5244
5244
  if (firstSeg === "..") {
5245
5245
  return {
5246
5246
  valid: false,
5247
5247
  reason: "Path traversal not allowed"
5248
5248
  };
5249
5249
  }
5250
- if (!isWithinDirectory(path22.resolve(vaultPath2, notePath), vaultPath2)) {
5250
+ if (!isWithinDirectory(path23.resolve(vaultPath2, notePath), vaultPath2)) {
5251
5251
  return {
5252
5252
  valid: false,
5253
5253
  reason: "Path traversal not allowed"
@@ -5260,7 +5260,7 @@ async function validatePathSecure(vaultPath2, notePath) {
5260
5260
  };
5261
5261
  }
5262
5262
  try {
5263
- const fullPath = path22.join(vaultPath2, notePath);
5263
+ const fullPath = path23.join(vaultPath2, notePath);
5264
5264
  try {
5265
5265
  await fs20.access(fullPath);
5266
5266
  const realPath = await fs20.realpath(fullPath);
@@ -5271,7 +5271,7 @@ async function validatePathSecure(vaultPath2, notePath) {
5271
5271
  reason: "Symlink target is outside vault"
5272
5272
  };
5273
5273
  }
5274
- const relativePath = path22.relative(realVaultPath, realPath);
5274
+ const relativePath = path23.relative(realVaultPath, realPath);
5275
5275
  if (isSensitivePath(relativePath)) {
5276
5276
  return {
5277
5277
  valid: false,
@@ -5279,7 +5279,7 @@ async function validatePathSecure(vaultPath2, notePath) {
5279
5279
  };
5280
5280
  }
5281
5281
  } catch {
5282
- const parentDir = path22.dirname(fullPath);
5282
+ const parentDir = path23.dirname(fullPath);
5283
5283
  try {
5284
5284
  await fs20.access(parentDir);
5285
5285
  const realParentPath = await fs20.realpath(parentDir);
@@ -6058,7 +6058,7 @@ var init_content_mutation = __esm({
6058
6058
 
6059
6059
  // src/core/write/file-io.ts
6060
6060
  import fs21 from "fs/promises";
6061
- import path23 from "path";
6061
+ import path24 from "path";
6062
6062
  import matter5 from "gray-matter";
6063
6063
  import { createHash as createHash2 } from "node:crypto";
6064
6064
  function computeContentHash(rawContent) {
@@ -6069,7 +6069,7 @@ async function readVaultFile(vaultPath2, notePath) {
6069
6069
  if (!validation.valid) {
6070
6070
  throw new Error(`Invalid path: ${validation.reason}`);
6071
6071
  }
6072
- const fullPath = path23.join(vaultPath2, notePath);
6072
+ const fullPath = path24.join(vaultPath2, notePath);
6073
6073
  const [rawContent, stat4] = await Promise.all([
6074
6074
  fs21.readFile(fullPath, "utf-8"),
6075
6075
  fs21.stat(fullPath)
@@ -6124,7 +6124,7 @@ async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEn
6124
6124
  if (!validation.valid) {
6125
6125
  throw new Error(`Invalid path: ${validation.reason}`);
6126
6126
  }
6127
- const fullPath = path23.join(vaultPath2, notePath);
6127
+ const fullPath = path24.join(vaultPath2, notePath);
6128
6128
  if (expectedHash) {
6129
6129
  const currentRaw = await fs21.readFile(fullPath, "utf-8");
6130
6130
  const currentHash = computeContentHash(currentRaw);
@@ -6192,8 +6192,8 @@ function createContext(variables = {}) {
6192
6192
  steps: {}
6193
6193
  };
6194
6194
  }
6195
- function resolvePath(obj, path40) {
6196
- const parts = path40.split(".");
6195
+ function resolvePath(obj, path41) {
6196
+ const parts = path41.split(".");
6197
6197
  let current = obj;
6198
6198
  for (const part of parts) {
6199
6199
  if (current === void 0 || current === null) {
@@ -6651,7 +6651,7 @@ __export(conditions_exports, {
6651
6651
  shouldStepExecute: () => shouldStepExecute
6652
6652
  });
6653
6653
  import fs29 from "fs/promises";
6654
- import path31 from "path";
6654
+ import path32 from "path";
6655
6655
  async function evaluateCondition(condition, vaultPath2, context) {
6656
6656
  const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
6657
6657
  const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
@@ -6704,7 +6704,7 @@ async function evaluateCondition(condition, vaultPath2, context) {
6704
6704
  }
6705
6705
  }
6706
6706
  async function evaluateFileExists(vaultPath2, notePath, expectExists) {
6707
- const fullPath = path31.join(vaultPath2, notePath);
6707
+ const fullPath = path32.join(vaultPath2, notePath);
6708
6708
  try {
6709
6709
  await fs29.access(fullPath);
6710
6710
  return {
@@ -6719,7 +6719,7 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
6719
6719
  }
6720
6720
  }
6721
6721
  async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
6722
- const fullPath = path31.join(vaultPath2, notePath);
6722
+ const fullPath = path32.join(vaultPath2, notePath);
6723
6723
  try {
6724
6724
  await fs29.access(fullPath);
6725
6725
  } catch {
@@ -6750,7 +6750,7 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
6750
6750
  }
6751
6751
  }
6752
6752
  async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
6753
- const fullPath = path31.join(vaultPath2, notePath);
6753
+ const fullPath = path32.join(vaultPath2, notePath);
6754
6754
  try {
6755
6755
  await fs29.access(fullPath);
6756
6756
  } catch {
@@ -6781,7 +6781,7 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
6781
6781
  }
6782
6782
  }
6783
6783
  async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
6784
- const fullPath = path31.join(vaultPath2, notePath);
6784
+ const fullPath = path32.join(vaultPath2, notePath);
6785
6785
  try {
6786
6786
  await fs29.access(fullPath);
6787
6787
  } catch {
@@ -6925,10 +6925,10 @@ var init_taskHelpers = __esm({
6925
6925
  });
6926
6926
 
6927
6927
  // 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";
6928
+ import * as path40 from "path";
6929
+ import { readFileSync as readFileSync6, realpathSync, existsSync as existsSync4 } from "fs";
6930
+ import { fileURLToPath as fileURLToPath4 } from "url";
6931
+ import { dirname as dirname8, join as join21 } from "path";
6932
6932
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6933
6933
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6934
6934
  import { performance as performance2 } from "node:perf_hooks";
@@ -7165,8 +7165,8 @@ function updateIndexProgress(parsed, total) {
7165
7165
  function normalizeTarget(target) {
7166
7166
  return target.toLowerCase().replace(/\.md$/, "");
7167
7167
  }
7168
- function normalizeNotePath(path40) {
7169
- return path40.toLowerCase().replace(/\.md$/, "");
7168
+ function normalizeNotePath(path41) {
7169
+ return path41.toLowerCase().replace(/\.md$/, "");
7170
7170
  }
7171
7171
  async function buildVaultIndex(vaultPath2, options = {}) {
7172
7172
  const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
@@ -7214,7 +7214,7 @@ async function buildVaultIndexInternal(vaultPath2, startTime, onProgress) {
7214
7214
  console.error(`Parsed ${parsedCount}/${files.length} files (${elapsed}s)`);
7215
7215
  onProgress?.(parsedCount, files.length);
7216
7216
  }
7217
- await new Promise((resolve3) => setImmediate(resolve3));
7217
+ await new Promise((resolve4) => setImmediate(resolve4));
7218
7218
  }
7219
7219
  if (parseErrors.length > 0) {
7220
7220
  const msg = `Failed to parse ${parseErrors.length} file(s):`;
@@ -7340,7 +7340,7 @@ function findSimilarEntity(index, target) {
7340
7340
  }
7341
7341
  const maxDist = normalizedLen <= 10 ? 1 : 2;
7342
7342
  let bestMatch;
7343
- for (const [entity, path40] of index.entities) {
7343
+ for (const [entity, path41] of index.entities) {
7344
7344
  const lenDiff = Math.abs(entity.length - normalizedLen);
7345
7345
  if (lenDiff > maxDist) {
7346
7346
  continue;
@@ -7348,7 +7348,7 @@ function findSimilarEntity(index, target) {
7348
7348
  const dist = levenshteinDistance(normalized, entity);
7349
7349
  if (dist > 0 && dist <= maxDist) {
7350
7350
  if (!bestMatch || dist < bestMatch.distance) {
7351
- bestMatch = { path: path40, entity, distance: dist };
7351
+ bestMatch = { path: path41, entity, distance: dist };
7352
7352
  if (dist === 1) {
7353
7353
  return bestMatch;
7354
7354
  }
@@ -7907,30 +7907,30 @@ var EventQueue = class {
7907
7907
  * Add a new event to the queue
7908
7908
  */
7909
7909
  push(type, rawPath) {
7910
- const path40 = normalizePath(rawPath);
7910
+ const path41 = normalizePath(rawPath);
7911
7911
  const now = Date.now();
7912
7912
  const event = {
7913
7913
  type,
7914
- path: path40,
7914
+ path: path41,
7915
7915
  timestamp: now
7916
7916
  };
7917
- let pending = this.pending.get(path40);
7917
+ let pending = this.pending.get(path41);
7918
7918
  if (!pending) {
7919
7919
  pending = {
7920
7920
  events: [],
7921
7921
  timer: null,
7922
7922
  lastEvent: now
7923
7923
  };
7924
- this.pending.set(path40, pending);
7924
+ this.pending.set(path41, pending);
7925
7925
  }
7926
7926
  pending.events.push(event);
7927
7927
  pending.lastEvent = now;
7928
- console.error(`[flywheel] QUEUE: pushed ${type} for ${path40}, pending=${this.pending.size}`);
7928
+ console.error(`[flywheel] QUEUE: pushed ${type} for ${path41}, pending=${this.pending.size}`);
7929
7929
  if (pending.timer) {
7930
7930
  clearTimeout(pending.timer);
7931
7931
  }
7932
7932
  pending.timer = setTimeout(() => {
7933
- this.flushPath(path40);
7933
+ this.flushPath(path41);
7934
7934
  }, this.config.debounceMs);
7935
7935
  if (this.pending.size >= this.config.batchSize) {
7936
7936
  this.flush();
@@ -7951,10 +7951,10 @@ var EventQueue = class {
7951
7951
  /**
7952
7952
  * Flush a single path's events
7953
7953
  */
7954
- flushPath(path40) {
7955
- const pending = this.pending.get(path40);
7954
+ flushPath(path41) {
7955
+ const pending = this.pending.get(path41);
7956
7956
  if (!pending || pending.events.length === 0) return;
7957
- console.error(`[flywheel] QUEUE: flushing ${path40}, events=${pending.events.length}`);
7957
+ console.error(`[flywheel] QUEUE: flushing ${path41}, events=${pending.events.length}`);
7958
7958
  if (pending.timer) {
7959
7959
  clearTimeout(pending.timer);
7960
7960
  pending.timer = null;
@@ -7963,7 +7963,7 @@ var EventQueue = class {
7963
7963
  if (coalescedType) {
7964
7964
  const coalesced = {
7965
7965
  type: coalescedType,
7966
- path: path40,
7966
+ path: path41,
7967
7967
  originalEvents: [...pending.events]
7968
7968
  };
7969
7969
  this.onBatch({
@@ -7972,7 +7972,7 @@ var EventQueue = class {
7972
7972
  timestamp: Date.now()
7973
7973
  });
7974
7974
  }
7975
- this.pending.delete(path40);
7975
+ this.pending.delete(path41);
7976
7976
  }
7977
7977
  /**
7978
7978
  * Flush all pending events
@@ -7984,7 +7984,7 @@ var EventQueue = class {
7984
7984
  }
7985
7985
  if (this.pending.size === 0) return;
7986
7986
  const events = [];
7987
- for (const [path40, pending] of this.pending) {
7987
+ for (const [path41, pending] of this.pending) {
7988
7988
  if (pending.timer) {
7989
7989
  clearTimeout(pending.timer);
7990
7990
  }
@@ -7992,7 +7992,7 @@ var EventQueue = class {
7992
7992
  if (coalescedType) {
7993
7993
  events.push({
7994
7994
  type: coalescedType,
7995
- path: path40,
7995
+ path: path41,
7996
7996
  originalEvents: [...pending.events]
7997
7997
  });
7998
7998
  }
@@ -8292,7 +8292,7 @@ async function processBatch(index, vaultPath2, batch, options = {}) {
8292
8292
  }
8293
8293
  onProgress?.(processed, total);
8294
8294
  if (processed % YIELD_INTERVAL === 0 && processed < total) {
8295
- await new Promise((resolve3) => setImmediate(resolve3));
8295
+ await new Promise((resolve4) => setImmediate(resolve4));
8296
8296
  }
8297
8297
  }
8298
8298
  const durationMs = Date.now() - startTime;
@@ -8384,31 +8384,31 @@ function createVaultWatcher(options) {
8384
8384
  usePolling: config2.usePolling,
8385
8385
  interval: config2.usePolling ? config2.pollInterval : void 0
8386
8386
  });
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);
8387
+ watcher.on("add", (path41) => {
8388
+ console.error(`[flywheel] RAW EVENT: add ${path41}`);
8389
+ if (shouldWatch(path41, vaultPath2)) {
8390
+ console.error(`[flywheel] ACCEPTED: add ${path41}`);
8391
+ eventQueue.push("add", path41);
8392
8392
  } else {
8393
- console.error(`[flywheel] FILTERED: add ${path40}`);
8393
+ console.error(`[flywheel] FILTERED: add ${path41}`);
8394
8394
  }
8395
8395
  });
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);
8396
+ watcher.on("change", (path41) => {
8397
+ console.error(`[flywheel] RAW EVENT: change ${path41}`);
8398
+ if (shouldWatch(path41, vaultPath2)) {
8399
+ console.error(`[flywheel] ACCEPTED: change ${path41}`);
8400
+ eventQueue.push("change", path41);
8401
8401
  } else {
8402
- console.error(`[flywheel] FILTERED: change ${path40}`);
8402
+ console.error(`[flywheel] FILTERED: change ${path41}`);
8403
8403
  }
8404
8404
  });
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);
8405
+ watcher.on("unlink", (path41) => {
8406
+ console.error(`[flywheel] RAW EVENT: unlink ${path41}`);
8407
+ if (shouldWatch(path41, vaultPath2)) {
8408
+ console.error(`[flywheel] ACCEPTED: unlink ${path41}`);
8409
+ eventQueue.push("unlink", path41);
8410
8410
  } else {
8411
- console.error(`[flywheel] FILTERED: unlink ${path40}`);
8411
+ console.error(`[flywheel] FILTERED: unlink ${path41}`);
8412
8412
  }
8413
8413
  });
8414
8414
  watcher.on("ready", () => {
@@ -8448,8 +8448,6 @@ import {
8448
8448
  getProtectedZones,
8449
8449
  rangeOverlapsProtectedZone,
8450
8450
  detectImplicitEntities as detectImplicitEntities2,
8451
- checkDbIntegrity,
8452
- safeBackupAsync,
8453
8451
  recordEntityMention as recordEntityMention2
8454
8452
  } from "@velvetmonkey/vault-core";
8455
8453
  init_serverLog();
@@ -9740,6 +9738,17 @@ var PipelineRunner = class {
9740
9738
  suggestionResults = [];
9741
9739
  async run() {
9742
9740
  const { p, tracker } = this;
9741
+ if (p.ctx.integrityState === "failed") {
9742
+ serverLog("watcher", `Skipping batch for ${p.ctx.name}: StateDb integrity failed`, "warn");
9743
+ this.activity.busy = false;
9744
+ this.activity.current_step = null;
9745
+ this.activity.last_completed_at = Date.now();
9746
+ this.activity.last_completed_trigger = "watcher";
9747
+ this.activity.last_completed_duration_ms = 0;
9748
+ this.activity.last_completed_files = p.events.length;
9749
+ this.activity.last_completed_steps = [];
9750
+ return;
9751
+ }
9743
9752
  this.activity.busy = true;
9744
9753
  this.activity.trigger = "watcher";
9745
9754
  this.activity.started_at = this.batchStart;
@@ -10625,21 +10634,15 @@ var PipelineRunner = class {
10625
10634
  async integrityCheck() {
10626
10635
  const { p } = this;
10627
10636
  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 {
10637
+ const result = await p.runIntegrityCheck(p.ctx, "watcher");
10638
+ if (result.status === "healthy") {
10639
+ return { integrity: "ok", backed_up: result.backupCreated };
10640
+ }
10641
+ if (result.status === "failed") {
10640
10642
  serverLog("watcher", `Integrity check FAILED: ${result.detail}`, "error");
10641
10643
  return { integrity: "failed", detail: result.detail };
10642
10644
  }
10645
+ return { skipped: true, reason: result.detail ?? "integrity runner unavailable" };
10643
10646
  }
10644
10647
  // ── Maintenance: periodic incremental vacuum ─────────────────────
10645
10648
  async maintenance() {
@@ -10999,8 +11002,77 @@ function getToolSelectionReport(stateDb2, daysBack = 7) {
10999
11002
  };
11000
11003
  }
11001
11004
 
11005
+ // src/core/read/integrity.ts
11006
+ import * as path16 from "node:path";
11007
+ import { existsSync as existsSync3 } from "node:fs";
11008
+ import { Worker as Worker2 } from "node:worker_threads";
11009
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
11010
+ var INTEGRITY_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
11011
+ var INTEGRITY_CHECK_TIMEOUT_MS = 2 * 60 * 1e3;
11012
+ var INTEGRITY_BACKUP_INTERVAL_MS = 6 * 60 * 60 * 1e3;
11013
+ var INTEGRITY_METADATA_KEYS = {
11014
+ checkedAt: "last_integrity_check",
11015
+ status: "last_integrity_status",
11016
+ durationMs: "last_integrity_duration_ms",
11017
+ detail: "last_integrity_detail"
11018
+ };
11019
+ function resolveWorkerSpec() {
11020
+ const thisFile = fileURLToPath2(import.meta.url);
11021
+ const thisDir = path16.dirname(thisFile);
11022
+ const prodPath = path16.join(thisDir, "integrity-worker.js");
11023
+ if (existsSync3(prodPath)) return { filename: prodPath };
11024
+ const distPath = path16.resolve(thisDir, "..", "..", "..", "dist", "integrity-worker.js");
11025
+ if (existsSync3(distPath)) return { filename: distPath };
11026
+ const srcPath = path16.join(thisDir, "integrity-worker.ts");
11027
+ return { filename: srcPath, execArgv: ["--import", "tsx"] };
11028
+ }
11029
+ async function runIntegrityWorker(message, timeoutMs = INTEGRITY_CHECK_TIMEOUT_MS) {
11030
+ const workerSpec = resolveWorkerSpec();
11031
+ return new Promise((resolve4) => {
11032
+ const worker2 = new Worker2(workerSpec.filename, {
11033
+ execArgv: workerSpec.execArgv
11034
+ });
11035
+ let settled = false;
11036
+ const finish = (result) => {
11037
+ if (settled) return;
11038
+ settled = true;
11039
+ clearTimeout(timer2);
11040
+ void worker2.terminate().catch(() => {
11041
+ });
11042
+ resolve4(result);
11043
+ };
11044
+ const timer2 = setTimeout(() => {
11045
+ finish({
11046
+ status: "error",
11047
+ detail: `Integrity worker timed out after ${timeoutMs}ms`,
11048
+ durationMs: timeoutMs,
11049
+ backupCreated: false
11050
+ });
11051
+ }, timeoutMs);
11052
+ worker2.once("message", (result) => finish(result));
11053
+ worker2.once("error", (err) => {
11054
+ finish({
11055
+ status: "error",
11056
+ detail: err.message,
11057
+ durationMs: 0,
11058
+ backupCreated: false
11059
+ });
11060
+ });
11061
+ worker2.once("exit", (code) => {
11062
+ if (settled || code === 0) return;
11063
+ finish({
11064
+ status: "error",
11065
+ detail: `Integrity worker exited with code ${code}`,
11066
+ durationMs: 0,
11067
+ backupCreated: false
11068
+ });
11069
+ });
11070
+ worker2.postMessage(message);
11071
+ });
11072
+ }
11073
+
11002
11074
  // 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";
11075
+ import { openStateDb, scanVaultEntities as scanVaultEntities5, getAllEntitiesFromDb as getAllEntitiesFromDb6, loadContentHashes, saveContentHashBatch, renameContentHash } from "@velvetmonkey/vault-core";
11004
11076
 
11005
11077
  // src/core/write/memory.ts
11006
11078
  init_wikilinkFeedback();
@@ -12074,8 +12146,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
12074
12146
  }
12075
12147
  }
12076
12148
  }
12077
- return Array.from(noteMap.entries()).map(([path40, stats]) => ({
12078
- path: path40,
12149
+ return Array.from(noteMap.entries()).map(([path41, stats]) => ({
12150
+ path: path41,
12079
12151
  access_count: stats.access_count,
12080
12152
  last_accessed: stats.last_accessed,
12081
12153
  tools_used: Array.from(stats.tools)
@@ -12762,10 +12834,10 @@ Use "flywheel_config" to inspect runtime configuration and set "tool_tier_overri
12762
12834
  }
12763
12835
 
12764
12836
  // src/tool-registry.ts
12765
- import * as path38 from "path";
12766
- import { dirname as dirname5, join as join18 } from "path";
12837
+ import * as path39 from "path";
12838
+ import { dirname as dirname6, join as join19 } from "path";
12767
12839
  import { statSync as statSync6, readFileSync as readFileSync5 } from "fs";
12768
- import { fileURLToPath as fileURLToPath2 } from "url";
12840
+ import { fileURLToPath as fileURLToPath3 } from "url";
12769
12841
  import { z as z39 } from "zod";
12770
12842
  import { CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
12771
12843
  import { getSessionId } from "@velvetmonkey/vault-core";
@@ -13114,13 +13186,13 @@ function multiHopBackfill(primaryResults, index, stateDb2, config2 = {}) {
13114
13186
  candidates.sort((a, b) => b.score - a.score);
13115
13187
  return candidates.slice(0, cfg.maxBackfill).map((c) => c.result);
13116
13188
  }
13117
- function scoreCandidate(path40, index, stateDb2) {
13118
- const note = index.notes.get(path40);
13189
+ function scoreCandidate(path41, index, stateDb2) {
13190
+ const note = index.notes.get(path41);
13119
13191
  const decay = recencyDecay(note?.modified);
13120
13192
  let hubScore = 1;
13121
13193
  if (stateDb2) {
13122
13194
  try {
13123
- const title = note?.title ?? path40.replace(/\.md$/, "").split("/").pop() ?? "";
13195
+ const title = note?.title ?? path41.replace(/\.md$/, "").split("/").pop() ?? "";
13124
13196
  const entity = getEntityByName3(stateDb2, title);
13125
13197
  if (entity) hubScore = entity.hubScore ?? 1;
13126
13198
  } catch {
@@ -13357,7 +13429,7 @@ init_stemmer();
13357
13429
 
13358
13430
  // src/tools/read/structure.ts
13359
13431
  import * as fs12 from "fs";
13360
- import * as path16 from "path";
13432
+ import * as path17 from "path";
13361
13433
  var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
13362
13434
  function extractHeadings2(content) {
13363
13435
  const lines = content.replace(/\r\n/g, "\n").split("\n");
@@ -13411,7 +13483,7 @@ function buildSections(headings, totalLines) {
13411
13483
  async function getNoteStructure(index, notePath, vaultPath2) {
13412
13484
  const note = index.notes.get(notePath);
13413
13485
  if (!note) return null;
13414
- const absolutePath = path16.join(vaultPath2, notePath);
13486
+ const absolutePath = path17.join(vaultPath2, notePath);
13415
13487
  let content;
13416
13488
  try {
13417
13489
  content = await fs12.promises.readFile(absolutePath, "utf-8");
@@ -13435,7 +13507,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
13435
13507
  async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
13436
13508
  const note = index.notes.get(notePath);
13437
13509
  if (!note) return null;
13438
- const absolutePath = path16.join(vaultPath2, notePath);
13510
+ const absolutePath = path17.join(vaultPath2, notePath);
13439
13511
  let content;
13440
13512
  try {
13441
13513
  content = await fs12.promises.readFile(absolutePath, "utf-8");
@@ -13478,7 +13550,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
13478
13550
  const results = [];
13479
13551
  for (const note of index.notes.values()) {
13480
13552
  if (folder && !note.path.startsWith(folder)) continue;
13481
- const absolutePath = path16.join(vaultPath2, note.path);
13553
+ const absolutePath = path17.join(vaultPath2, note.path);
13482
13554
  let content;
13483
13555
  try {
13484
13556
  content = await fs12.promises.readFile(absolutePath, "utf-8");
@@ -13580,11 +13652,11 @@ function applyEntityBridging(results, stateDb2, maxBridgesPerResult = 5) {
13580
13652
  const linkMap = /* @__PURE__ */ new Map();
13581
13653
  try {
13582
13654
  const paths = results.map((r) => r.path).filter(Boolean);
13583
- for (const path40 of paths) {
13655
+ for (const path41 of paths) {
13584
13656
  const rows = stateDb2.db.prepare(
13585
13657
  "SELECT target FROM note_links WHERE note_path = ?"
13586
- ).all(path40);
13587
- linkMap.set(path40, new Set(rows.map((r) => r.target)));
13658
+ ).all(path41);
13659
+ linkMap.set(path41, new Set(rows.map((r) => r.target)));
13588
13660
  }
13589
13661
  } catch {
13590
13662
  return;
@@ -14137,7 +14209,7 @@ init_vault_scope();
14137
14209
 
14138
14210
  // src/tools/read/graph.ts
14139
14211
  import * as fs13 from "fs";
14140
- import * as path17 from "path";
14212
+ import * as path18 from "path";
14141
14213
  import { z as z2 } from "zod";
14142
14214
 
14143
14215
  // src/tools/read/graphAdvanced.ts
@@ -14571,7 +14643,7 @@ function detectCycles(index, maxLength = 10, limit = 20) {
14571
14643
  // src/tools/read/graph.ts
14572
14644
  async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
14573
14645
  try {
14574
- const fullPath = path17.join(vaultPath2, sourcePath);
14646
+ const fullPath = path18.join(vaultPath2, sourcePath);
14575
14647
  const content = await fs13.promises.readFile(fullPath, "utf-8");
14576
14648
  const allLines = content.split("\n");
14577
14649
  let fmLines = 0;
@@ -15116,14 +15188,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath, getStateDb4 = ()
15116
15188
  };
15117
15189
  function findSimilarEntity2(target, entities) {
15118
15190
  const targetLower = target.toLowerCase();
15119
- for (const [name, path40] of entities) {
15191
+ for (const [name, path41] of entities) {
15120
15192
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
15121
- return path40;
15193
+ return path41;
15122
15194
  }
15123
15195
  }
15124
- for (const [name, path40] of entities) {
15196
+ for (const [name, path41] of entities) {
15125
15197
  if (name.includes(targetLower) || targetLower.includes(name)) {
15126
- return path40;
15198
+ return path41;
15127
15199
  }
15128
15200
  }
15129
15201
  return void 0;
@@ -15666,7 +15738,17 @@ function getProactiveLinkingOneLiner(stateDb2, daysBack = 1) {
15666
15738
  init_wikilinkFeedback();
15667
15739
  init_embeddings();
15668
15740
  var STALE_THRESHOLD_SECONDS = 300;
15669
- function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb4 = () => null, getWatcherStatus2 = () => null, getVersion = () => "unknown", getPipelineActivityState = () => null) {
15741
+ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb4 = () => null, getWatcherStatus2 = () => null, getVersion = () => "unknown", getPipelineActivityState = () => null, getVaultRuntimeState = () => ({
15742
+ bootState: "booting",
15743
+ integrityState: "unknown",
15744
+ integrityCheckInProgress: false,
15745
+ integrityStartedAt: null,
15746
+ integritySource: null,
15747
+ lastIntegrityCheckedAt: null,
15748
+ lastIntegrityDurationMs: null,
15749
+ lastIntegrityDetail: null,
15750
+ lastBackupAt: null
15751
+ })) {
15670
15752
  const IndexProgressSchema = z4.object({
15671
15753
  parsed: z4.coerce.number().describe("Number of files parsed so far"),
15672
15754
  total: z4.coerce.number().describe("Total number of files to parse")
@@ -15819,18 +15901,14 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15819
15901
  }
15820
15902
  let dbIntegrityFailed = false;
15821
15903
  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
- }
15904
+ const runtimeState = getVaultRuntimeState();
15905
+ if (runtimeState.integrityState === "failed") {
15906
+ dbIntegrityFailed = true;
15907
+ recommendations.push(`Database integrity check failed: ${runtimeState.lastIntegrityDetail ?? "unknown integrity failure"}`);
15908
+ } else if (runtimeState.integrityState === "error") {
15909
+ recommendations.push(`Database integrity check error: ${runtimeState.lastIntegrityDetail ?? "integrity runner error"}`);
15910
+ } else if (runtimeState.integrityCheckInProgress) {
15911
+ recommendations.push("Database integrity check is still running.");
15834
15912
  }
15835
15913
  const indexBuilt = indexState2 === "ready" && index !== void 0 && index.notes !== void 0;
15836
15914
  let lastIndexActivityAt;
@@ -16024,6 +16102,14 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16024
16102
  tasks_ready: isTaskCacheReady(),
16025
16103
  tasks_building: isTaskCacheBuilding(),
16026
16104
  watcher_state: getWatcherStatus2()?.state,
16105
+ boot_state: runtimeState.bootState,
16106
+ integrity_state: runtimeState.integrityState,
16107
+ integrity_check_in_progress: runtimeState.integrityCheckInProgress,
16108
+ integrity_started_at: runtimeState.integrityStartedAt,
16109
+ integrity_source: runtimeState.integritySource,
16110
+ integrity_last_checked_at: runtimeState.lastIntegrityCheckedAt,
16111
+ integrity_duration_ms: runtimeState.lastIntegrityDurationMs,
16112
+ integrity_detail: runtimeState.lastIntegrityDetail,
16027
16113
  watcher_pending: getWatcherStatus2()?.pendingEvents,
16028
16114
  last_index_activity_at: lastIndexActivityAt,
16029
16115
  last_index_activity_ago_seconds: lastIndexActivityAt ? Math.floor((Date.now() - lastIndexActivityAt) / 1e3) : void 0,
@@ -16075,6 +16161,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16075
16161
  async ({ detail = false }) => {
16076
16162
  const activity = getPipelineActivityState();
16077
16163
  const now = Date.now();
16164
+ const runtimeState = getVaultRuntimeState();
16078
16165
  const output = {
16079
16166
  busy: activity?.busy ?? false,
16080
16167
  trigger: activity?.trigger ?? null,
@@ -16083,6 +16170,9 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16083
16170
  current_step: activity?.current_step ?? null,
16084
16171
  progress: activity && activity.busy && activity.total_steps > 0 ? `${activity.completed_steps}/${activity.total_steps} steps` : null,
16085
16172
  pending_events: activity?.pending_events ?? 0,
16173
+ boot_state: runtimeState.bootState,
16174
+ integrity_state: runtimeState.integrityState,
16175
+ integrity_check_in_progress: runtimeState.integrityCheckInProgress,
16086
16176
  last_completed: activity?.last_completed_at ? {
16087
16177
  at: activity.last_completed_at,
16088
16178
  ago_seconds: Math.floor((now - activity.last_completed_at) / 1e3),
@@ -16143,8 +16233,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16143
16233
  daily_counts: z4.record(z4.number())
16144
16234
  }).describe("Activity summary for the last 7 days")
16145
16235
  };
16146
- function isPeriodicNote3(path40) {
16147
- const filename = path40.split("/").pop() || "";
16236
+ function isPeriodicNote3(path41) {
16237
+ const filename = path41.split("/").pop() || "";
16148
16238
  const nameWithoutExt = filename.replace(/\.md$/, "");
16149
16239
  const patterns = [
16150
16240
  /^\d{4}-\d{2}-\d{2}$/,
@@ -16159,7 +16249,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16159
16249
  // YYYY (yearly)
16160
16250
  ];
16161
16251
  const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
16162
- const folder = path40.split("/")[0]?.toLowerCase() || "";
16252
+ const folder = path41.split("/")[0]?.toLowerCase() || "";
16163
16253
  return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
16164
16254
  }
16165
16255
  async function runVaultStats() {
@@ -17266,13 +17356,13 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17266
17356
  max_content_chars: z6.number().default(2e4).describe("Max total chars of section content to include. Sections are truncated at paragraph boundaries.")
17267
17357
  }
17268
17358
  },
17269
- async ({ path: path40, include_content, max_content_chars }) => {
17359
+ async ({ path: path41, include_content, max_content_chars }) => {
17270
17360
  const index = getIndex();
17271
17361
  const vaultPath2 = getVaultPath();
17272
- const result = await getNoteStructure(index, path40, vaultPath2);
17362
+ const result = await getNoteStructure(index, path41, vaultPath2);
17273
17363
  if (!result) {
17274
17364
  return {
17275
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path40 }, null, 2) }]
17365
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path41 }, null, 2) }]
17276
17366
  };
17277
17367
  }
17278
17368
  let totalChars = 0;
@@ -17283,7 +17373,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17283
17373
  truncated = true;
17284
17374
  break;
17285
17375
  }
17286
- const sectionResult = await getSectionContent(index, path40, section.heading.text, vaultPath2, true);
17376
+ const sectionResult = await getSectionContent(index, path41, section.heading.text, vaultPath2, true);
17287
17377
  if (sectionResult) {
17288
17378
  let content = sectionResult.content;
17289
17379
  const remaining = max_content_chars - totalChars;
@@ -17298,13 +17388,13 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17298
17388
  }
17299
17389
  }
17300
17390
  }
17301
- const note = index.notes.get(path40);
17391
+ const note = index.notes.get(path41);
17302
17392
  const enriched = { ...result };
17303
17393
  if (note) {
17304
17394
  enriched.frontmatter = note.frontmatter;
17305
17395
  enriched.tags = note.tags;
17306
17396
  enriched.aliases = note.aliases;
17307
- const normalizedPath = path40.toLowerCase().replace(/\.md$/, "");
17397
+ const normalizedPath = path41.toLowerCase().replace(/\.md$/, "");
17308
17398
  const backlinks = index.backlinks.get(normalizedPath) || [];
17309
17399
  enriched.backlink_count = backlinks.length;
17310
17400
  enriched.outlink_count = note.outlinks.length;
@@ -17342,15 +17432,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17342
17432
  max_content_chars: z6.number().default(1e4).describe("Max chars of section content. Truncated at paragraph boundaries.")
17343
17433
  }
17344
17434
  },
17345
- async ({ path: path40, heading, include_subheadings, max_content_chars }) => {
17435
+ async ({ path: path41, heading, include_subheadings, max_content_chars }) => {
17346
17436
  const index = getIndex();
17347
17437
  const vaultPath2 = getVaultPath();
17348
- const result = await getSectionContent(index, path40, heading, vaultPath2, include_subheadings);
17438
+ const result = await getSectionContent(index, path41, heading, vaultPath2, include_subheadings);
17349
17439
  if (!result) {
17350
17440
  return {
17351
17441
  content: [{ type: "text", text: JSON.stringify({
17352
17442
  error: "Section not found",
17353
- path: path40,
17443
+ path: path41,
17354
17444
  heading
17355
17445
  }, null, 2) }]
17356
17446
  };
@@ -17411,16 +17501,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17411
17501
  offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
17412
17502
  }
17413
17503
  },
17414
- async ({ path: path40, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
17504
+ async ({ path: path41, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
17415
17505
  const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
17416
17506
  const index = getIndex();
17417
17507
  const vaultPath2 = getVaultPath();
17418
17508
  const config2 = getConfig2();
17419
- if (path40) {
17420
- const result2 = await getTasksFromNote(index, path40, vaultPath2, getExcludeTags(config2));
17509
+ if (path41) {
17510
+ const result2 = await getTasksFromNote(index, path41, vaultPath2, getExcludeTags(config2));
17421
17511
  if (!result2) {
17422
17512
  return {
17423
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path40 }, null, 2) }]
17513
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path41 }, null, 2) }]
17424
17514
  };
17425
17515
  }
17426
17516
  let filtered = result2;
@@ -17430,7 +17520,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17430
17520
  const paged2 = filtered.slice(offset, offset + limit);
17431
17521
  return {
17432
17522
  content: [{ type: "text", text: JSON.stringify({
17433
- path: path40,
17523
+ path: path41,
17434
17524
  total_count: filtered.length,
17435
17525
  returned_count: paged2.length,
17436
17526
  open: result2.filter((t) => t.status === "open").length,
@@ -17586,7 +17676,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17586
17676
  // src/tools/read/migrations.ts
17587
17677
  import { z as z7 } from "zod";
17588
17678
  import * as fs15 from "fs/promises";
17589
- import * as path18 from "path";
17679
+ import * as path19 from "path";
17590
17680
  import matter2 from "gray-matter";
17591
17681
  function getNotesInFolder(index, folder) {
17592
17682
  const notes = [];
@@ -17599,7 +17689,7 @@ function getNotesInFolder(index, folder) {
17599
17689
  return notes;
17600
17690
  }
17601
17691
  async function readFileContent(notePath, vaultPath2) {
17602
- const fullPath = path18.join(vaultPath2, notePath);
17692
+ const fullPath = path19.join(vaultPath2, notePath);
17603
17693
  try {
17604
17694
  return await fs15.readFile(fullPath, "utf-8");
17605
17695
  } catch {
@@ -17607,7 +17697,7 @@ async function readFileContent(notePath, vaultPath2) {
17607
17697
  }
17608
17698
  }
17609
17699
  async function writeFileContent(notePath, vaultPath2, content) {
17610
- const fullPath = path18.join(vaultPath2, notePath);
17700
+ const fullPath = path19.join(vaultPath2, notePath);
17611
17701
  try {
17612
17702
  await fs15.writeFile(fullPath, content, "utf-8");
17613
17703
  return true;
@@ -17788,7 +17878,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
17788
17878
 
17789
17879
  // src/tools/read/graphAnalysis.ts
17790
17880
  import fs16 from "node:fs";
17791
- import path19 from "node:path";
17881
+ import path20 from "node:path";
17792
17882
  import { z as z8 } from "zod";
17793
17883
 
17794
17884
  // src/tools/read/schema.ts
@@ -18314,7 +18404,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb4
18314
18404
  const scored = allNotes.map((note) => {
18315
18405
  let wordCount = 0;
18316
18406
  try {
18317
- const content = fs16.readFileSync(path19.join(vaultPath2, note.path), "utf-8");
18407
+ const content = fs16.readFileSync(path20.join(vaultPath2, note.path), "utf-8");
18318
18408
  const body = content.replace(/^---[\s\S]*?---\n?/, "");
18319
18409
  wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
18320
18410
  } catch {
@@ -18945,12 +19035,12 @@ import { z as z11 } from "zod";
18945
19035
 
18946
19036
  // src/tools/read/bidirectional.ts
18947
19037
  import * as fs17 from "fs/promises";
18948
- import * as path20 from "path";
19038
+ import * as path21 from "path";
18949
19039
  import matter3 from "gray-matter";
18950
19040
  var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
18951
19041
  var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
18952
19042
  async function readFileContent2(notePath, vaultPath2) {
18953
- const fullPath = path20.join(vaultPath2, notePath);
19043
+ const fullPath = path21.join(vaultPath2, notePath);
18954
19044
  try {
18955
19045
  return await fs17.readFile(fullPath, "utf-8");
18956
19046
  } catch {
@@ -19129,10 +19219,10 @@ async function suggestWikilinksInFrontmatter(index, notePath, vaultPath2) {
19129
19219
 
19130
19220
  // src/tools/read/computed.ts
19131
19221
  import * as fs18 from "fs/promises";
19132
- import * as path21 from "path";
19222
+ import * as path22 from "path";
19133
19223
  import matter4 from "gray-matter";
19134
19224
  async function readFileContent3(notePath, vaultPath2) {
19135
- const fullPath = path21.join(vaultPath2, notePath);
19225
+ const fullPath = path22.join(vaultPath2, notePath);
19136
19226
  try {
19137
19227
  return await fs18.readFile(fullPath, "utf-8");
19138
19228
  } catch {
@@ -19140,7 +19230,7 @@ async function readFileContent3(notePath, vaultPath2) {
19140
19230
  }
19141
19231
  }
19142
19232
  async function getFileStats(notePath, vaultPath2) {
19143
- const fullPath = path21.join(vaultPath2, notePath);
19233
+ const fullPath = path22.join(vaultPath2, notePath);
19144
19234
  try {
19145
19235
  const stats = await fs18.stat(fullPath);
19146
19236
  return {
@@ -19413,7 +19503,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath, getConfi
19413
19503
  init_writer();
19414
19504
  import { z as z12 } from "zod";
19415
19505
  import fs24 from "fs/promises";
19416
- import path26 from "path";
19506
+ import path27 from "path";
19417
19507
 
19418
19508
  // src/core/write/validator.ts
19419
19509
  var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
@@ -19632,16 +19722,16 @@ init_writer();
19632
19722
  init_wikilinks();
19633
19723
  init_wikilinkFeedback();
19634
19724
  import fs23 from "fs/promises";
19635
- import path25 from "path";
19725
+ import path26 from "path";
19636
19726
 
19637
19727
  // src/core/write/policy/policyPaths.ts
19638
19728
  import fs22 from "fs/promises";
19639
- import path24 from "path";
19729
+ import path25 from "path";
19640
19730
  function getPoliciesDir(vaultPath2) {
19641
- return path24.join(vaultPath2, ".flywheel", "policies");
19731
+ return path25.join(vaultPath2, ".flywheel", "policies");
19642
19732
  }
19643
19733
  function getLegacyPoliciesDir(vaultPath2) {
19644
- return path24.join(vaultPath2, ".claude", "policies");
19734
+ return path25.join(vaultPath2, ".claude", "policies");
19645
19735
  }
19646
19736
  async function ensurePoliciesDir(vaultPath2) {
19647
19737
  const dir = getPoliciesDir(vaultPath2);
@@ -19658,14 +19748,14 @@ async function migratePoliciesIfNeeded(vaultPath2) {
19658
19748
  const yamlFiles = files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
19659
19749
  if (yamlFiles.length === 0) {
19660
19750
  await tryRmdir(legacyDir);
19661
- await tryRmdir(path24.join(vaultPath2, ".claude"));
19751
+ await tryRmdir(path25.join(vaultPath2, ".claude"));
19662
19752
  return;
19663
19753
  }
19664
19754
  await ensurePoliciesDir(vaultPath2);
19665
19755
  const destDir = getPoliciesDir(vaultPath2);
19666
19756
  for (const file of yamlFiles) {
19667
- const src = path24.join(legacyDir, file);
19668
- const dest = path24.join(destDir, file);
19757
+ const src = path25.join(legacyDir, file);
19758
+ const dest = path25.join(destDir, file);
19669
19759
  try {
19670
19760
  await fs22.access(dest);
19671
19761
  } catch {
@@ -19674,7 +19764,7 @@ async function migratePoliciesIfNeeded(vaultPath2) {
19674
19764
  await fs22.unlink(src);
19675
19765
  }
19676
19766
  await tryRmdir(legacyDir);
19677
- await tryRmdir(path24.join(vaultPath2, ".claude"));
19767
+ await tryRmdir(path25.join(vaultPath2, ".claude"));
19678
19768
  }
19679
19769
  async function tryRmdir(dir) {
19680
19770
  try {
@@ -19687,7 +19777,7 @@ async function tryRmdir(dir) {
19687
19777
  }
19688
19778
  var migrationCache = /* @__PURE__ */ new Map();
19689
19779
  async function ensureMigrated(vaultPath2) {
19690
- const key = path24.resolve(vaultPath2);
19780
+ const key = path25.resolve(vaultPath2);
19691
19781
  if (!migrationCache.has(key)) {
19692
19782
  migrationCache.set(key, migratePoliciesIfNeeded(vaultPath2));
19693
19783
  }
@@ -19754,7 +19844,7 @@ async function getPolicyHint(vaultPath2) {
19754
19844
  return "";
19755
19845
  }
19756
19846
  async function ensureFileExists(vaultPath2, notePath) {
19757
- const fullPath = path25.join(vaultPath2, notePath);
19847
+ const fullPath = path26.join(vaultPath2, notePath);
19758
19848
  try {
19759
19849
  await fs23.access(fullPath);
19760
19850
  return null;
@@ -19959,7 +20049,7 @@ async function executeCreateNote(options) {
19959
20049
  if (!pathCheck.valid) {
19960
20050
  return { success: false, result: errorResult(notePath, `Path blocked: ${pathCheck.reason}`), filesWritten: [] };
19961
20051
  }
19962
- const fullPath = path25.join(vaultPath2, notePath);
20052
+ const fullPath = path26.join(vaultPath2, notePath);
19963
20053
  let fileExists = false;
19964
20054
  try {
19965
20055
  await fs23.access(fullPath);
@@ -19969,7 +20059,7 @@ async function executeCreateNote(options) {
19969
20059
  if (fileExists && !overwrite) {
19970
20060
  return { success: false, result: errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`), filesWritten: [] };
19971
20061
  }
19972
- await fs23.mkdir(path25.dirname(fullPath), { recursive: true });
20062
+ await fs23.mkdir(path26.dirname(fullPath), { recursive: true });
19973
20063
  const { maybeApplyWikilinks: maybeApplyWikilinks2 } = await Promise.resolve().then(() => (init_wikilinks(), wikilinks_exports));
19974
20064
  const { content: processedContent } = maybeApplyWikilinks2(content, skipWikilinks ?? false, notePath);
19975
20065
  let finalFrontmatter = frontmatter;
@@ -20003,7 +20093,7 @@ async function executeDeleteNote(options) {
20003
20093
  if (!pathCheck.valid) {
20004
20094
  return { success: false, result: errorResult(notePath, `Path blocked: ${pathCheck.reason}`), filesWritten: [] };
20005
20095
  }
20006
- const fullPath = path25.join(vaultPath2, notePath);
20096
+ const fullPath = path26.join(vaultPath2, notePath);
20007
20097
  try {
20008
20098
  await fs23.access(fullPath);
20009
20099
  } catch {
@@ -20027,10 +20117,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20027
20117
  if (!validation.valid) {
20028
20118
  throw new Error(`Path blocked: ${validation.reason}`);
20029
20119
  }
20030
- const fullPath = path26.join(vaultPath2, notePath);
20031
- await fs24.mkdir(path26.dirname(fullPath), { recursive: true });
20120
+ const fullPath = path27.join(vaultPath2, notePath);
20121
+ await fs24.mkdir(path27.dirname(fullPath), { recursive: true });
20032
20122
  const templates = config2.templates || {};
20033
- const filename = path26.basename(notePath, ".md").toLowerCase();
20123
+ const filename = path27.basename(notePath, ".md").toLowerCase();
20034
20124
  let templatePath;
20035
20125
  let periodicType;
20036
20126
  const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
@@ -20063,7 +20153,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20063
20153
  ];
20064
20154
  for (const candidate of candidates) {
20065
20155
  try {
20066
- await fs24.access(path26.join(vaultPath2, candidate));
20156
+ await fs24.access(path27.join(vaultPath2, candidate));
20067
20157
  templatePath = candidate;
20068
20158
  console.error(`[Flywheel] Template not in config but found at ${candidate} \u2014 using it`);
20069
20159
  break;
@@ -20074,11 +20164,11 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20074
20164
  let templateContent;
20075
20165
  if (templatePath) {
20076
20166
  try {
20077
- const absTemplatePath = path26.join(vaultPath2, templatePath);
20167
+ const absTemplatePath = path27.join(vaultPath2, templatePath);
20078
20168
  templateContent = await fs24.readFile(absTemplatePath, "utf-8");
20079
20169
  } catch {
20080
20170
  console.error(`[Flywheel] Template at ${templatePath} not readable, using minimal fallback`);
20081
- const title = path26.basename(notePath, ".md");
20171
+ const title = path27.basename(notePath, ".md");
20082
20172
  templateContent = `---
20083
20173
  ---
20084
20174
 
@@ -20090,7 +20180,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20090
20180
  if (periodicType) {
20091
20181
  console.error(`[Flywheel] No ${periodicType} template found in config or vault \u2014 using minimal fallback`);
20092
20182
  }
20093
- const title = path26.basename(notePath, ".md");
20183
+ const title = path27.basename(notePath, ".md");
20094
20184
  templateContent = `---
20095
20185
  ---
20096
20186
 
@@ -20099,7 +20189,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
20099
20189
  }
20100
20190
  const now = /* @__PURE__ */ new Date();
20101
20191
  const dateStr = now.toISOString().split("T")[0];
20102
- templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path26.basename(notePath, ".md"));
20192
+ templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path27.basename(notePath, ".md"));
20103
20193
  const matter9 = (await import("gray-matter")).default;
20104
20194
  const parsed = matter9(templateContent);
20105
20195
  if (!parsed.data.date) {
@@ -20138,7 +20228,7 @@ function registerMutationTools(server2, getVaultPath, getConfig2 = () => ({})) {
20138
20228
  let noteCreated = false;
20139
20229
  let templateUsed;
20140
20230
  if (create_if_missing && !dry_run) {
20141
- const fullPath = path26.join(vaultPath2, notePath);
20231
+ const fullPath = path27.join(vaultPath2, notePath);
20142
20232
  try {
20143
20233
  await fs24.access(fullPath);
20144
20234
  } catch {
@@ -20149,7 +20239,7 @@ function registerMutationTools(server2, getVaultPath, getConfig2 = () => ({})) {
20149
20239
  }
20150
20240
  }
20151
20241
  if (create_if_missing && dry_run) {
20152
- const fullPath = path26.join(vaultPath2, notePath);
20242
+ const fullPath = path27.join(vaultPath2, notePath);
20153
20243
  try {
20154
20244
  await fs24.access(fullPath);
20155
20245
  } catch {
@@ -20636,7 +20726,7 @@ init_writer();
20636
20726
  init_wikilinks();
20637
20727
  import { z as z15 } from "zod";
20638
20728
  import fs25 from "fs/promises";
20639
- import path27 from "path";
20729
+ import path28 from "path";
20640
20730
  function registerNoteTools(server2, getVaultPath, getIndex) {
20641
20731
  server2.tool(
20642
20732
  "vault_create_note",
@@ -20662,23 +20752,23 @@ function registerNoteTools(server2, getVaultPath, getIndex) {
20662
20752
  if (!validatePath(vaultPath2, notePath)) {
20663
20753
  return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
20664
20754
  }
20665
- const fullPath = path27.join(vaultPath2, notePath);
20755
+ const fullPath = path28.join(vaultPath2, notePath);
20666
20756
  const existsCheck = await ensureFileExists(vaultPath2, notePath);
20667
20757
  if (existsCheck === null && !overwrite) {
20668
20758
  return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
20669
20759
  }
20670
- const dir = path27.dirname(fullPath);
20760
+ const dir = path28.dirname(fullPath);
20671
20761
  await fs25.mkdir(dir, { recursive: true });
20672
20762
  let effectiveContent = content;
20673
20763
  let effectiveFrontmatter = frontmatter;
20674
20764
  if (template) {
20675
- const templatePath = path27.join(vaultPath2, template);
20765
+ const templatePath = path28.join(vaultPath2, template);
20676
20766
  try {
20677
20767
  const raw = await fs25.readFile(templatePath, "utf-8");
20678
20768
  const matter9 = (await import("gray-matter")).default;
20679
20769
  const parsed = matter9(raw);
20680
20770
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
20681
- const title = path27.basename(notePath, ".md");
20771
+ const title = path28.basename(notePath, ".md");
20682
20772
  let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
20683
20773
  if (content) {
20684
20774
  templateContent = templateContent.trimEnd() + "\n\n" + content;
@@ -20697,7 +20787,7 @@ function registerNoteTools(server2, getVaultPath, getIndex) {
20697
20787
  effectiveFrontmatter.created = now.toISOString();
20698
20788
  }
20699
20789
  const warnings = [];
20700
- const noteName = path27.basename(notePath, ".md");
20790
+ const noteName = path28.basename(notePath, ".md");
20701
20791
  const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
20702
20792
  const preflight = await checkPreflightSimilarity(noteName);
20703
20793
  if (preflight.existingEntity) {
@@ -20838,7 +20928,7 @@ ${sources}`;
20838
20928
  }
20839
20929
  return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
20840
20930
  }
20841
- const fullPath = path27.join(vaultPath2, notePath);
20931
+ const fullPath = path28.join(vaultPath2, notePath);
20842
20932
  await fs25.unlink(fullPath);
20843
20933
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
20844
20934
  const message = backlinkWarning ? `Deleted note: ${notePath}
@@ -20860,7 +20950,7 @@ init_git();
20860
20950
  init_wikilinks();
20861
20951
  import { z as z16 } from "zod";
20862
20952
  import fs26 from "fs/promises";
20863
- import path28 from "path";
20953
+ import path29 from "path";
20864
20954
  import matter6 from "gray-matter";
20865
20955
  function escapeRegex(str) {
20866
20956
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -20879,7 +20969,7 @@ function extractWikilinks2(content) {
20879
20969
  return wikilinks;
20880
20970
  }
20881
20971
  function getTitleFromPath(filePath) {
20882
- return path28.basename(filePath, ".md");
20972
+ return path29.basename(filePath, ".md");
20883
20973
  }
20884
20974
  async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
20885
20975
  const results = [];
@@ -20888,7 +20978,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
20888
20978
  const files = [];
20889
20979
  const entries = await fs26.readdir(dir, { withFileTypes: true });
20890
20980
  for (const entry of entries) {
20891
- const fullPath = path28.join(dir, entry.name);
20981
+ const fullPath = path29.join(dir, entry.name);
20892
20982
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
20893
20983
  files.push(...await scanDir(fullPath));
20894
20984
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -20899,7 +20989,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
20899
20989
  }
20900
20990
  const allFiles = await scanDir(vaultPath2);
20901
20991
  for (const filePath of allFiles) {
20902
- const relativePath = path28.relative(vaultPath2, filePath);
20992
+ const relativePath = path29.relative(vaultPath2, filePath);
20903
20993
  const content = await fs26.readFile(filePath, "utf-8");
20904
20994
  const wikilinks = extractWikilinks2(content);
20905
20995
  const matchingLinks = [];
@@ -20986,8 +21076,8 @@ function registerMoveNoteTools(server2, getVaultPath) {
20986
21076
  };
20987
21077
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
20988
21078
  }
20989
- const oldFullPath = path28.join(vaultPath2, oldPath);
20990
- const newFullPath = path28.join(vaultPath2, newPath);
21079
+ const oldFullPath = path29.join(vaultPath2, oldPath);
21080
+ const newFullPath = path29.join(vaultPath2, newPath);
20991
21081
  try {
20992
21082
  await fs26.access(oldFullPath);
20993
21083
  } catch {
@@ -21068,7 +21158,7 @@ function registerMoveNoteTools(server2, getVaultPath) {
21068
21158
  };
21069
21159
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
21070
21160
  }
21071
- const destDir = path28.dirname(newFullPath);
21161
+ const destDir = path29.dirname(newFullPath);
21072
21162
  await fs26.mkdir(destDir, { recursive: true });
21073
21163
  await fs26.rename(oldFullPath, newFullPath);
21074
21164
  let gitCommit;
@@ -21144,10 +21234,10 @@ function registerMoveNoteTools(server2, getVaultPath) {
21144
21234
  if (sanitizedTitle !== newTitle) {
21145
21235
  console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
21146
21236
  }
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);
21237
+ const fullPath = path29.join(vaultPath2, notePath);
21238
+ const dir = path29.dirname(notePath);
21239
+ const newPath = dir === "." ? `${sanitizedTitle}.md` : path29.join(dir, `${sanitizedTitle}.md`);
21240
+ const newFullPath = path29.join(vaultPath2, newPath);
21151
21241
  try {
21152
21242
  await fs26.access(fullPath);
21153
21243
  } catch {
@@ -21279,7 +21369,7 @@ init_writer();
21279
21369
  init_wikilinks();
21280
21370
  import { z as z17 } from "zod";
21281
21371
  import fs27 from "fs/promises";
21282
- import path29 from "path";
21372
+ import path30 from "path";
21283
21373
  function registerMergeTools(server2, getVaultPath) {
21284
21374
  server2.tool(
21285
21375
  "merge_entities",
@@ -21576,14 +21666,14 @@ async function findSourceNote(vaultPath2, sourceName, excludePath) {
21576
21666
  }
21577
21667
  for (const entry of entries) {
21578
21668
  if (entry.name.startsWith(".")) continue;
21579
- const fullPath = path29.join(dir, entry.name);
21669
+ const fullPath = path30.join(dir, entry.name);
21580
21670
  if (entry.isDirectory()) {
21581
21671
  const found = await scanDir(fullPath);
21582
21672
  if (found) return found;
21583
21673
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
21584
- const basename5 = path29.basename(entry.name, ".md");
21674
+ const basename5 = path30.basename(entry.name, ".md");
21585
21675
  if (basename5.toLowerCase() === targetLower) {
21586
- const relative2 = path29.relative(vaultPath2, fullPath).replace(/\\/g, "/");
21676
+ const relative2 = path30.relative(vaultPath2, fullPath).replace(/\\/g, "/");
21587
21677
  if (relative2 !== excludePath) return relative2;
21588
21678
  }
21589
21679
  }
@@ -21704,7 +21794,7 @@ Message: ${undoResult.undoneCommit.message}` : void 0
21704
21794
  }
21705
21795
 
21706
21796
  // src/tools/write/policy.ts
21707
- import * as path34 from "path";
21797
+ import * as path35 from "path";
21708
21798
  import { z as z20 } from "zod";
21709
21799
 
21710
21800
  // src/core/write/policy/index.ts
@@ -21714,7 +21804,7 @@ init_schema();
21714
21804
  // src/core/write/policy/parser.ts
21715
21805
  init_schema();
21716
21806
  import fs28 from "fs/promises";
21717
- import path30 from "path";
21807
+ import path31 from "path";
21718
21808
  import matter7 from "gray-matter";
21719
21809
  function parseYaml(content) {
21720
21810
  const parsed = matter7(`---
@@ -21765,12 +21855,12 @@ async function loadPolicyFile(filePath) {
21765
21855
  async function loadPolicy(vaultPath2, policyName) {
21766
21856
  await ensureMigrated(vaultPath2);
21767
21857
  const policiesDir = getPoliciesDir(vaultPath2);
21768
- const policyPath = path30.join(policiesDir, `${policyName}.yaml`);
21858
+ const policyPath = path31.join(policiesDir, `${policyName}.yaml`);
21769
21859
  try {
21770
21860
  await fs28.access(policyPath);
21771
21861
  return loadPolicyFile(policyPath);
21772
21862
  } catch {
21773
- const ymlPath = path30.join(policiesDir, `${policyName}.yml`);
21863
+ const ymlPath = path31.join(policiesDir, `${policyName}.yml`);
21774
21864
  try {
21775
21865
  await fs28.access(ymlPath);
21776
21866
  return loadPolicyFile(ymlPath);
@@ -21913,7 +22003,7 @@ init_writer();
21913
22003
  init_git();
21914
22004
  init_wikilinks();
21915
22005
  import fs30 from "fs/promises";
21916
- import path32 from "path";
22006
+ import path33 from "path";
21917
22007
  init_constants2();
21918
22008
  async function executeStep(step, vaultPath2, context, conditionResults, searchFn) {
21919
22009
  const { execute, reason } = shouldStepExecute(step.when, conditionResults);
@@ -22104,12 +22194,12 @@ async function executeCreateNote2(params, vaultPath2, context) {
22104
22194
  let frontmatter = params.frontmatter || {};
22105
22195
  if (params.template) {
22106
22196
  try {
22107
- const templatePath = path32.join(vaultPath2, String(params.template));
22197
+ const templatePath = path33.join(vaultPath2, String(params.template));
22108
22198
  const raw = await fs30.readFile(templatePath, "utf-8");
22109
22199
  const matter9 = (await import("gray-matter")).default;
22110
22200
  const parsed = matter9(raw);
22111
22201
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
22112
- const title = path32.basename(notePath, ".md");
22202
+ const title = path33.basename(notePath, ".md");
22113
22203
  let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
22114
22204
  if (content) {
22115
22205
  templateContent = templateContent.trimEnd() + "\n\n" + content;
@@ -22142,7 +22232,7 @@ async function executeToggleTask(params, vaultPath2) {
22142
22232
  const notePath = String(params.path || "");
22143
22233
  const task = String(params.task || "");
22144
22234
  const section = params.section ? String(params.section) : void 0;
22145
- const fullPath = path32.join(vaultPath2, notePath);
22235
+ const fullPath = path33.join(vaultPath2, notePath);
22146
22236
  try {
22147
22237
  await fs30.access(fullPath);
22148
22238
  } catch {
@@ -22425,7 +22515,7 @@ async function rollbackChanges(vaultPath2, originalContents, filesModified) {
22425
22515
  const pathCheck = await validatePathSecure(vaultPath2, filePath);
22426
22516
  if (!pathCheck.valid) continue;
22427
22517
  const original = originalContents.get(filePath);
22428
- const fullPath = path32.join(vaultPath2, filePath);
22518
+ const fullPath = path33.join(vaultPath2, filePath);
22429
22519
  if (original === null) {
22430
22520
  try {
22431
22521
  await fs30.unlink(fullPath);
@@ -22480,7 +22570,7 @@ async function previewPolicy(policy, vaultPath2, variables) {
22480
22570
 
22481
22571
  // src/core/write/policy/storage.ts
22482
22572
  import fs31 from "fs/promises";
22483
- import path33 from "path";
22573
+ import path34 from "path";
22484
22574
  async function listPolicies(vaultPath2) {
22485
22575
  await ensureMigrated(vaultPath2);
22486
22576
  const dir = getPoliciesDir(vaultPath2);
@@ -22491,7 +22581,7 @@ async function listPolicies(vaultPath2) {
22491
22581
  if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
22492
22582
  continue;
22493
22583
  }
22494
- const filePath = path33.join(dir, file);
22584
+ const filePath = path34.join(dir, file);
22495
22585
  const stat4 = await fs31.stat(filePath);
22496
22586
  const content = await fs31.readFile(filePath, "utf-8");
22497
22587
  const metadata = extractPolicyMetadata(content);
@@ -22517,7 +22607,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
22517
22607
  const dir = getPoliciesDir(vaultPath2);
22518
22608
  await ensurePoliciesDir(vaultPath2);
22519
22609
  const filename = `${policyName}.yaml`;
22520
- const filePath = path33.join(dir, filename);
22610
+ const filePath = path34.join(dir, filename);
22521
22611
  if (!overwrite) {
22522
22612
  try {
22523
22613
  await fs31.access(filePath);
@@ -22631,7 +22721,7 @@ function registerPolicyTools(server2, getVaultPath, getSearchFn) {
22631
22721
  const policies = await listPolicies(vaultPath2);
22632
22722
  const response = {
22633
22723
  success: true,
22634
- vault: path34.basename(vaultPath2),
22724
+ vault: path35.basename(vaultPath2),
22635
22725
  vault_path: vaultPath2,
22636
22726
  count: policies.length,
22637
22727
  policies: policies.map((p) => ({
@@ -23065,7 +23155,7 @@ import { z as z21 } from "zod";
23065
23155
 
23066
23156
  // src/core/write/tagRename.ts
23067
23157
  import * as fs32 from "fs/promises";
23068
- import * as path35 from "path";
23158
+ import * as path36 from "path";
23069
23159
  import matter8 from "gray-matter";
23070
23160
  import { getProtectedZones as getProtectedZones2 } from "@velvetmonkey/vault-core";
23071
23161
  function getNotesInFolder3(index, folder) {
@@ -23171,7 +23261,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
23171
23261
  const previews = [];
23172
23262
  let totalChanges = 0;
23173
23263
  for (const note of affectedNotes) {
23174
- const fullPath = path35.join(vaultPath2, note.path);
23264
+ const fullPath = path36.join(vaultPath2, note.path);
23175
23265
  let fileContent;
23176
23266
  try {
23177
23267
  fileContent = await fs32.readFile(fullPath, "utf-8");
@@ -24301,7 +24391,7 @@ init_wikilinks();
24301
24391
  init_wikilinkFeedback();
24302
24392
  import { z as z28 } from "zod";
24303
24393
  import * as fs33 from "fs/promises";
24304
- import * as path36 from "path";
24394
+ import * as path37 from "path";
24305
24395
  import { scanVaultEntities as scanVaultEntities4, SCHEMA_VERSION as SCHEMA_VERSION2 } from "@velvetmonkey/vault-core";
24306
24396
  init_embeddings();
24307
24397
  function hasSkipWikilinks(content) {
@@ -24317,13 +24407,13 @@ async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
24317
24407
  const entries = await fs33.readdir(dirPath, { withFileTypes: true });
24318
24408
  for (const entry of entries) {
24319
24409
  if (entry.name.startsWith(".")) continue;
24320
- const fullPath = path36.join(dirPath, entry.name);
24410
+ const fullPath = path37.join(dirPath, entry.name);
24321
24411
  if (entry.isDirectory()) {
24322
24412
  if (excludeFolders.some((f) => entry.name.toLowerCase() === f.toLowerCase())) continue;
24323
24413
  const sub = await collectMarkdownFiles(fullPath, basePath, excludeFolders);
24324
24414
  results.push(...sub);
24325
24415
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
24326
- results.push(path36.relative(basePath, fullPath));
24416
+ results.push(path37.relative(basePath, fullPath));
24327
24417
  }
24328
24418
  }
24329
24419
  } catch {
@@ -24353,7 +24443,7 @@ var EXCLUDE_FOLDERS = [
24353
24443
  ];
24354
24444
  function buildStatusReport(stateDb2, vaultPath2) {
24355
24445
  const recommendations = [];
24356
- const dbPath = path36.join(vaultPath2, ".flywheel", "state.db");
24446
+ const dbPath = path37.join(vaultPath2, ".flywheel", "state.db");
24357
24447
  const statedbExists = stateDb2 !== null;
24358
24448
  if (!statedbExists) {
24359
24449
  recommendations.push("StateDb not initialized \u2014 server needs restart");
@@ -24480,7 +24570,7 @@ async function executeRun(stateDb2, vaultPath2) {
24480
24570
  const allFiles = await collectMarkdownFiles(vaultPath2, vaultPath2, EXCLUDE_FOLDERS);
24481
24571
  let eligible = 0;
24482
24572
  for (const relativePath of allFiles) {
24483
- const fullPath = path36.join(vaultPath2, relativePath);
24573
+ const fullPath = path37.join(vaultPath2, relativePath);
24484
24574
  let content;
24485
24575
  try {
24486
24576
  content = await fs33.readFile(fullPath, "utf-8");
@@ -24538,7 +24628,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
24538
24628
  const eligible = [];
24539
24629
  let notesSkipped = 0;
24540
24630
  for (const relativePath of allFiles) {
24541
- const fullPath = path36.join(vaultPath2, relativePath);
24631
+ const fullPath = path37.join(vaultPath2, relativePath);
24542
24632
  let content;
24543
24633
  try {
24544
24634
  content = await fs33.readFile(fullPath, "utf-8");
@@ -24568,7 +24658,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
24568
24658
  match_count: result.linksAdded
24569
24659
  });
24570
24660
  if (!dryRun) {
24571
- const fullPath = path36.join(vaultPath2, relativePath);
24661
+ const fullPath = path37.join(vaultPath2, relativePath);
24572
24662
  await fs33.writeFile(fullPath, result.content, "utf-8");
24573
24663
  notesModified++;
24574
24664
  if (stateDb2) {
@@ -24747,7 +24837,7 @@ import { z as z30 } from "zod";
24747
24837
 
24748
24838
  // src/core/read/similarity.ts
24749
24839
  import * as fs34 from "fs";
24750
- import * as path37 from "path";
24840
+ import * as path38 from "path";
24751
24841
  init_embeddings();
24752
24842
 
24753
24843
  // src/core/read/mmr.ts
@@ -24817,7 +24907,7 @@ function extractKeyTerms(content, maxTerms = 15) {
24817
24907
  }
24818
24908
  function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
24819
24909
  const limit = options.limit ?? 10;
24820
- const absPath = path37.join(vaultPath2, sourcePath);
24910
+ const absPath = path38.join(vaultPath2, sourcePath);
24821
24911
  let content;
24822
24912
  try {
24823
24913
  content = fs34.readFileSync(absPath, "utf-8");
@@ -24959,7 +25049,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb4) {
24959
25049
  diversity: z30.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
24960
25050
  }
24961
25051
  },
24962
- async ({ path: path40, limit, diversity }) => {
25052
+ async ({ path: path41, limit, diversity }) => {
24963
25053
  const index = getIndex();
24964
25054
  const vaultPath2 = getVaultPath();
24965
25055
  const stateDb2 = getStateDb4();
@@ -24968,10 +25058,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb4) {
24968
25058
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
24969
25059
  };
24970
25060
  }
24971
- if (!index.notes.has(path40)) {
25061
+ if (!index.notes.has(path41)) {
24972
25062
  return {
24973
25063
  content: [{ type: "text", text: JSON.stringify({
24974
- error: `Note not found: ${path40}`,
25064
+ error: `Note not found: ${path41}`,
24975
25065
  hint: "Use the full relative path including .md extension"
24976
25066
  }, null, 2) }]
24977
25067
  };
@@ -24983,12 +25073,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb4) {
24983
25073
  };
24984
25074
  const useHybrid = hasEmbeddingsIndex();
24985
25075
  const method = useHybrid ? "hybrid" : "bm25";
24986
- const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path40, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path40, opts);
25076
+ const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path41, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path41, opts);
24987
25077
  return {
24988
25078
  content: [{
24989
25079
  type: "text",
24990
25080
  text: JSON.stringify({
24991
- source: path40,
25081
+ source: path41,
24992
25082
  method,
24993
25083
  count: results.length,
24994
25084
  similar: results
@@ -26862,9 +26952,9 @@ function registerVaultResources(server2, getIndex) {
26862
26952
  }
26863
26953
 
26864
26954
  // 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"));
26955
+ var __trFilename = fileURLToPath3(import.meta.url);
26956
+ var __trDirname = dirname6(__trFilename);
26957
+ var trPkg = JSON.parse(readFileSync5(join19(__trDirname, "../package.json"), "utf-8"));
26868
26958
  var ACTIVATION_PATTERNS = [
26869
26959
  {
26870
26960
  category: "graph",
@@ -26902,6 +26992,34 @@ var ACTIVATION_PATTERNS = [
26902
26992
  patterns: [/\b(delete note|move note|rename note|merge entities|merge notes?)\b/i]
26903
26993
  }
26904
26994
  ];
26995
+ var MUTATING_TOOL_NAMES = /* @__PURE__ */ new Set([
26996
+ "vault_add_to_section",
26997
+ "vault_remove_from_section",
26998
+ "vault_replace_in_section",
26999
+ "vault_add_task",
27000
+ "vault_toggle_task",
27001
+ "vault_update_frontmatter",
27002
+ "vault_create_note",
27003
+ "vault_delete_note",
27004
+ "vault_move_note",
27005
+ "vault_rename_note",
27006
+ "merge_entities",
27007
+ "absorb_as_alias",
27008
+ "vault_undo_last_mutation",
27009
+ "policy",
27010
+ "rename_tag",
27011
+ "wikilink_feedback",
27012
+ "tool_selection_feedback",
27013
+ "vault_record_correction",
27014
+ "vault_resolve_correction",
27015
+ "memory",
27016
+ "flywheel_config",
27017
+ "vault_init",
27018
+ "rename_field",
27019
+ "migrate_field_values",
27020
+ "refresh_index",
27021
+ "init_semantic"
27022
+ ]);
26905
27023
  function getPatternSignals(raw) {
26906
27024
  if (!raw) return [];
26907
27025
  return ACTIVATION_PATTERNS.filter(({ patterns }) => patterns.some((pattern) => pattern.test(raw))).map(({ category, tier }) => ({ category, tier }));
@@ -27094,7 +27212,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27094
27212
  let totalBytes = 0;
27095
27213
  for (const p of notePaths) {
27096
27214
  try {
27097
- totalBytes += statSync6(path38.join(vp, p)).size;
27215
+ totalBytes += statSync6(path39.join(vp, p)).size;
27098
27216
  } catch {
27099
27217
  }
27100
27218
  }
@@ -27127,6 +27245,26 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27127
27245
  };
27128
27246
  }
27129
27247
  const isMultiVault = registry?.isMultiVault ?? false;
27248
+ function getTargetVaultContext(params) {
27249
+ if (!registry) return null;
27250
+ if (isMultiVault) {
27251
+ const vaultName = typeof params?.vault === "string" ? params.vault : void 0;
27252
+ return registry.getContext(vaultName);
27253
+ }
27254
+ return registry.getContext();
27255
+ }
27256
+ function wrapWithIntegrityGate(toolName, handler) {
27257
+ if (!MUTATING_TOOL_NAMES.has(toolName)) return handler;
27258
+ return async (...args) => {
27259
+ const params = args[0] && typeof args[0] === "object" ? args[0] : void 0;
27260
+ const vaultCtx = getTargetVaultContext(params);
27261
+ const integrityState = vaultCtx?.integrityState ?? getActiveScopeOrNull()?.integrityState;
27262
+ if (integrityState === "failed") {
27263
+ throw new Error("StateDb integrity failed; write operations are disabled until recovery/restart.");
27264
+ }
27265
+ return handler(...args);
27266
+ };
27267
+ }
27130
27268
  function wrapWithVaultActivation(toolName, handler) {
27131
27269
  if (!isMultiVault || !registry || !vaultCallbacks) return handler;
27132
27270
  return async (...args) => {
@@ -27242,6 +27380,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27242
27380
  if (args.length > 0 && typeof args[args.length - 1] === "function") {
27243
27381
  let handler = args[args.length - 1];
27244
27382
  handler = wrapWithVaultActivation(name, handler);
27383
+ handler = wrapWithIntegrityGate(name, handler);
27245
27384
  args[args.length - 1] = wrapWithTracking(name, handler);
27246
27385
  }
27247
27386
  const registered = origTool(name, ...args);
@@ -27256,6 +27395,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27256
27395
  if (args.length > 0 && typeof args[args.length - 1] === "function") {
27257
27396
  let handler = args[args.length - 1];
27258
27397
  handler = wrapWithVaultActivation(name, handler);
27398
+ handler = wrapWithIntegrityGate(name, handler);
27259
27399
  args[args.length - 1] = wrapWithTracking(name, handler);
27260
27400
  }
27261
27401
  const registered = origRegisterTool(name, ...args);
@@ -27349,7 +27489,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
27349
27489
  }
27350
27490
  function registerAllTools(targetServer, ctx, controller) {
27351
27491
  const { getVaultPath: gvp, getVaultIndex: gvi, getStateDb: gsd, getFlywheelConfig: gcf } = ctx;
27352
- registerHealthTools(targetServer, gvi, gvp, gcf, gsd, ctx.getWatcherStatus, () => trPkg.version, ctx.getPipelineActivity);
27492
+ registerHealthTools(targetServer, gvi, gvp, gcf, gsd, ctx.getWatcherStatus, () => trPkg.version, ctx.getPipelineActivity, ctx.getVaultRuntimeState);
27353
27493
  registerSystemTools(
27354
27494
  targetServer,
27355
27495
  gvi,
@@ -27445,9 +27585,9 @@ function registerAllTools(targetServer, ctx, controller) {
27445
27585
  }
27446
27586
 
27447
27587
  // 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"));
27588
+ var __filename2 = fileURLToPath4(import.meta.url);
27589
+ var __dirname = dirname8(__filename2);
27590
+ var pkg = JSON.parse(readFileSync6(join21(__dirname, "../package.json"), "utf-8"));
27451
27591
  var vaultPath;
27452
27592
  var resolvedVaultPath;
27453
27593
  var vaultIndex;
@@ -27463,6 +27603,7 @@ var lastMcpRequestAt = 0;
27463
27603
  var lastFullRebuildAt = 0;
27464
27604
  var startupScanFiles = null;
27465
27605
  var deferredScheduler = null;
27606
+ var integrityRuns = /* @__PURE__ */ new Map();
27466
27607
  function getWatcherStatus() {
27467
27608
  if (vaultRegistry) {
27468
27609
  const name = globalThis.__flywheel_active_vault;
@@ -27507,6 +27648,20 @@ function buildRegistryContext() {
27507
27648
  getFlywheelConfig: () => getActiveScopeOrNull()?.flywheelConfig ?? flywheelConfig,
27508
27649
  getWatcherStatus,
27509
27650
  getPipelineActivity: () => getActiveScopeOrNull()?.pipelineActivity ?? null,
27651
+ getVaultRuntimeState: () => {
27652
+ const scope = getActiveScopeOrNull();
27653
+ return {
27654
+ bootState: scope?.bootState ?? "booting",
27655
+ integrityState: scope?.integrityState ?? "unknown",
27656
+ integrityCheckInProgress: scope?.integrityCheckInProgress ?? false,
27657
+ integrityStartedAt: scope?.integrityStartedAt ?? null,
27658
+ integritySource: scope?.integritySource ?? null,
27659
+ lastIntegrityCheckedAt: scope?.lastIntegrityCheckedAt ?? null,
27660
+ lastIntegrityDurationMs: scope?.lastIntegrityDurationMs ?? null,
27661
+ lastIntegrityDetail: scope?.lastIntegrityDetail ?? null,
27662
+ lastBackupAt: scope?.lastBackupAt ?? null
27663
+ };
27664
+ },
27510
27665
  updateVaultIndex,
27511
27666
  updateFlywheelConfig
27512
27667
  };
@@ -27608,6 +27763,110 @@ function loadVaultCooccurrence(ctx) {
27608
27763
  serverLog("index", `[${ctx.name}] Co-occurrence: loaded from cache (${Object.keys(cachedCooc.index.associations).length} entities, ${cachedCooc.index._metadata.total_associations} associations)`);
27609
27764
  }
27610
27765
  }
27766
+ function hydrateIntegrityMetadata(ctx) {
27767
+ if (!ctx.stateDb) return;
27768
+ const checkedAtRow = ctx.stateDb.getMetadataValue.get(INTEGRITY_METADATA_KEYS.checkedAt);
27769
+ const statusRow = ctx.stateDb.getMetadataValue.get(INTEGRITY_METADATA_KEYS.status);
27770
+ const durationRow = ctx.stateDb.getMetadataValue.get(INTEGRITY_METADATA_KEYS.durationMs);
27771
+ const detailRow = ctx.stateDb.getMetadataValue.get(INTEGRITY_METADATA_KEYS.detail);
27772
+ ctx.lastIntegrityCheckedAt = checkedAtRow ? parseInt(checkedAtRow.value, 10) || null : null;
27773
+ ctx.lastIntegrityDurationMs = durationRow ? parseInt(durationRow.value, 10) || null : null;
27774
+ ctx.lastIntegrityDetail = detailRow?.value ? detailRow.value : null;
27775
+ const status = statusRow?.value;
27776
+ if (status === "healthy" || status === "failed" || status === "error") {
27777
+ ctx.integrityState = status;
27778
+ }
27779
+ }
27780
+ function setBootState(ctx, state2) {
27781
+ ctx.bootState = state2;
27782
+ if (globalThis.__flywheel_active_vault === ctx.name) {
27783
+ setActiveScope(buildVaultScope(ctx));
27784
+ }
27785
+ }
27786
+ function setIntegrityState(ctx, state2, detail = ctx.lastIntegrityDetail, durationMs = ctx.lastIntegrityDurationMs) {
27787
+ ctx.integrityState = state2;
27788
+ ctx.lastIntegrityDetail = detail;
27789
+ ctx.lastIntegrityDurationMs = durationMs;
27790
+ if (state2 === "failed") {
27791
+ ctx.bootState = "degraded";
27792
+ }
27793
+ if (globalThis.__flywheel_active_vault === ctx.name) {
27794
+ setActiveScope(buildVaultScope(ctx));
27795
+ }
27796
+ }
27797
+ function persistIntegrityMetadata(ctx) {
27798
+ if (!ctx.stateDb || ctx.lastIntegrityCheckedAt == null) return;
27799
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.checkedAt, String(ctx.lastIntegrityCheckedAt));
27800
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.status, ctx.integrityState);
27801
+ if (ctx.lastIntegrityDurationMs != null) {
27802
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.durationMs, String(ctx.lastIntegrityDurationMs));
27803
+ }
27804
+ if (ctx.lastIntegrityDetail) {
27805
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.detail, ctx.lastIntegrityDetail);
27806
+ } else {
27807
+ ctx.stateDb.setMetadataValue.run(INTEGRITY_METADATA_KEYS.detail, "");
27808
+ }
27809
+ }
27810
+ function shouldRunBackup(ctx) {
27811
+ if (ctx.lastBackupAt == null) return true;
27812
+ return Date.now() - ctx.lastBackupAt >= INTEGRITY_BACKUP_INTERVAL_MS;
27813
+ }
27814
+ async function runIntegrityCheck(ctx, source, options = {}) {
27815
+ if (!ctx.stateDb) {
27816
+ return { status: "error", detail: "StateDb not available", durationMs: 0, backupCreated: false };
27817
+ }
27818
+ if (!options.force && ctx.integrityState === "healthy" && ctx.lastIntegrityCheckedAt != null) {
27819
+ if (Date.now() - ctx.lastIntegrityCheckedAt < INTEGRITY_CHECK_INTERVAL_MS) {
27820
+ return {
27821
+ status: "healthy",
27822
+ detail: ctx.lastIntegrityDetail,
27823
+ durationMs: ctx.lastIntegrityDurationMs ?? 0,
27824
+ backupCreated: false
27825
+ };
27826
+ }
27827
+ }
27828
+ const existing = integrityRuns.get(ctx.name);
27829
+ if (existing) return existing;
27830
+ ctx.integrityCheckInProgress = true;
27831
+ ctx.integrityStartedAt = Date.now();
27832
+ ctx.integritySource = source;
27833
+ setIntegrityState(ctx, "checking", ctx.lastIntegrityDetail, ctx.lastIntegrityDurationMs);
27834
+ serverLog("statedb", `[${ctx.name}] Integrity check started (${source})`);
27835
+ const promise = runIntegrityWorker({
27836
+ dbPath: ctx.stateDb.dbPath,
27837
+ runBackup: shouldRunBackup(ctx),
27838
+ busyTimeoutMs: 5e3
27839
+ }).then((result) => {
27840
+ ctx.integrityCheckInProgress = false;
27841
+ ctx.integrityStartedAt = null;
27842
+ ctx.integritySource = source;
27843
+ ctx.lastIntegrityCheckedAt = Date.now();
27844
+ ctx.lastIntegrityDurationMs = result.durationMs;
27845
+ ctx.lastIntegrityDetail = result.detail;
27846
+ if (result.backupCreated) {
27847
+ ctx.lastBackupAt = Date.now();
27848
+ }
27849
+ if (result.status === "healthy") {
27850
+ setIntegrityState(ctx, "healthy", result.detail, result.durationMs);
27851
+ serverLog("statedb", `[${ctx.name}] Integrity check passed in ${result.durationMs}ms`);
27852
+ } else if (result.status === "failed") {
27853
+ setIntegrityState(ctx, "failed", result.detail, result.durationMs);
27854
+ serverLog("statedb", `[${ctx.name}] Integrity check failed: ${result.detail}`, "error");
27855
+ } else {
27856
+ setIntegrityState(ctx, "error", result.detail, result.durationMs);
27857
+ serverLog("statedb", `[${ctx.name}] Integrity check error: ${result.detail}`, "warn");
27858
+ }
27859
+ persistIntegrityMetadata(ctx);
27860
+ return result;
27861
+ }).finally(() => {
27862
+ integrityRuns.delete(ctx.name);
27863
+ if (globalThis.__flywheel_active_vault === ctx.name) {
27864
+ setActiveScope(buildVaultScope(ctx));
27865
+ }
27866
+ });
27867
+ integrityRuns.set(ctx.name, promise);
27868
+ return promise;
27869
+ }
27611
27870
  async function initializeVault(name, vaultPathArg) {
27612
27871
  const ctx = {
27613
27872
  name,
@@ -27625,25 +27884,21 @@ async function initializeVault(name, vaultPathArg) {
27625
27884
  lastEntityScanAt: 0,
27626
27885
  lastHubScoreRebuildAt: 0,
27627
27886
  lastIndexCacheSaveAt: 0,
27628
- pipelineActivity: createEmptyPipelineActivity()
27887
+ pipelineActivity: createEmptyPipelineActivity(),
27888
+ bootState: "booting",
27889
+ integrityState: "unknown",
27890
+ integrityCheckInProgress: false,
27891
+ integrityStartedAt: null,
27892
+ integritySource: null,
27893
+ lastIntegrityCheckedAt: null,
27894
+ lastIntegrityDurationMs: null,
27895
+ lastIntegrityDetail: null,
27896
+ lastBackupAt: null
27629
27897
  };
27630
27898
  try {
27631
27899
  ctx.stateDb = openStateDb(vaultPathArg);
27632
27900
  serverLog("statedb", `[${name}] StateDb initialized`);
27633
- const integrity = checkDbIntegrity2(ctx.stateDb.db);
27634
- if (integrity.ok) {
27635
- safeBackupAsync2(ctx.stateDb.db, ctx.stateDb.dbPath).catch((err) => {
27636
- serverLog("backup", `[${name}] Safe backup failed: ${err}`, "error");
27637
- });
27638
- } else {
27639
- serverLog("statedb", `[${name}] Integrity check failed: ${integrity.detail} \u2014 recreating`, "error");
27640
- const dbPath = ctx.stateDb.dbPath;
27641
- preserveCorruptedDb(dbPath);
27642
- ctx.stateDb.close();
27643
- deleteStateDbFiles(dbPath);
27644
- ctx.stateDb = openStateDb(vaultPathArg);
27645
- attemptSalvage(ctx.stateDb.db, dbPath);
27646
- }
27901
+ hydrateIntegrityMetadata(ctx);
27647
27902
  const vaultInitRow = ctx.stateDb.getMetadataValue.get("vault_init_last_run_at");
27648
27903
  if (!vaultInitRow) {
27649
27904
  serverLog("server", `[${name}] Vault not initialized \u2014 call vault_init to enrich legacy notes`);
@@ -27667,7 +27922,16 @@ function buildVaultScope(ctx) {
27667
27922
  indexError: ctx.indexError,
27668
27923
  embeddingsBuilding: ctx.embeddingsBuilding,
27669
27924
  entityEmbeddingsMap: getEntityEmbeddingsMap(),
27670
- pipelineActivity: ctx.pipelineActivity
27925
+ pipelineActivity: ctx.pipelineActivity,
27926
+ bootState: ctx.bootState,
27927
+ integrityState: ctx.integrityState,
27928
+ integrityCheckInProgress: ctx.integrityCheckInProgress,
27929
+ integrityStartedAt: ctx.integrityStartedAt,
27930
+ integritySource: ctx.integritySource,
27931
+ lastIntegrityCheckedAt: ctx.lastIntegrityCheckedAt,
27932
+ lastIntegrityDurationMs: ctx.lastIntegrityDurationMs,
27933
+ lastIntegrityDetail: ctx.lastIntegrityDetail,
27934
+ lastBackupAt: ctx.lastBackupAt
27671
27935
  };
27672
27936
  }
27673
27937
  function activateVault(ctx, skipEmbeddingLoad = false) {
@@ -27821,6 +28085,9 @@ async function bootVault(ctx, startTime) {
27821
28085
  serverLog("index", `[${ctx.name}] Failed to build vault index: ${err instanceof Error ? err.message : err}`, "error");
27822
28086
  }
27823
28087
  }
28088
+ if (ctx.bootState !== "degraded") {
28089
+ setBootState(ctx, "ready");
28090
+ }
27824
28091
  }
27825
28092
  async function main() {
27826
28093
  const vaultConfigs = parseVaultConfig();
@@ -27830,7 +28097,7 @@ async function main() {
27830
28097
  } catch {
27831
28098
  resolvedVaultPath = vaultPath.replace(/\\/g, "/");
27832
28099
  }
27833
- if (!existsSync3(resolvedVaultPath)) {
28100
+ if (!existsSync4(resolvedVaultPath)) {
27834
28101
  console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
27835
28102
  console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
27836
28103
  process.exit(1);
@@ -27845,14 +28112,17 @@ async function main() {
27845
28112
  vaultRegistry.addContext(primaryCtx2);
27846
28113
  stateDb = primaryCtx2.stateDb;
27847
28114
  activateVault(primaryCtx2, true);
28115
+ serverLog("server", `[${primaryCtx2.name}] stateDb_open=${Date.now() - startTime}ms`);
27848
28116
  } else {
27849
28117
  vaultRegistry = new VaultRegistry("default");
27850
28118
  const ctx = await initializeVault("default", vaultPath);
27851
28119
  vaultRegistry.addContext(ctx);
27852
28120
  stateDb = ctx.stateDb;
27853
28121
  activateVault(ctx, true);
28122
+ serverLog("server", `[${ctx.name}] stateDb_open=${Date.now() - startTime}ms`);
27854
28123
  }
27855
28124
  await initToolRouting();
28125
+ serverLog("server", `tool_routing=${Date.now() - startTime}ms`);
27856
28126
  if (stateDb) {
27857
28127
  try {
27858
28128
  const vaultName = vaultRegistry?.primaryName ?? "default";
@@ -27944,10 +28214,16 @@ async function main() {
27944
28214
  });
27945
28215
  }
27946
28216
  const primaryCtx = vaultRegistry.getContext();
28217
+ setBootState(primaryCtx, "transport_connected");
28218
+ serverLog("server", `[${primaryCtx.name}] transport_connect=${Date.now() - startTime}ms`);
28219
+ serverLog("server", `[${primaryCtx.name}] integrity_check_started=${Date.now() - startTime}ms`);
28220
+ void runIntegrityCheck(primaryCtx, "startup");
28221
+ setBootState(primaryCtx, "booting");
27947
28222
  loadVaultCooccurrence(primaryCtx);
27948
28223
  activateVault(primaryCtx);
27949
28224
  await bootVault(primaryCtx, startTime);
27950
28225
  activateVault(primaryCtx);
28226
+ serverLog("server", `[${primaryCtx.name}] boot_complete=${Date.now() - startTime}ms`);
27951
28227
  serverReady = true;
27952
28228
  const watchdogInterval = parseInt(process.env.FLYWHEEL_WATCHDOG_INTERVAL ?? "0", 10);
27953
28229
  if (watchdogInterval > 0 && (transportMode === "http" || transportMode === "both")) {
@@ -28009,6 +28285,9 @@ async function main() {
28009
28285
  const ctx = await initializeVault(vc.name, vc.path);
28010
28286
  vaultRegistry.addContext(ctx);
28011
28287
  invalidateHttpPool();
28288
+ setBootState(ctx, "transport_connected");
28289
+ void runIntegrityCheck(ctx, "startup");
28290
+ setBootState(ctx, "booting");
28012
28291
  loadVaultCooccurrence(ctx);
28013
28292
  activateVault(ctx);
28014
28293
  await bootVault(ctx, startTime);
@@ -28224,7 +28503,7 @@ async function runPostIndexWork(ctx) {
28224
28503
  if (attempt < MAX_BUILD_RETRIES) {
28225
28504
  const delay = 1e4;
28226
28505
  serverLog("semantic", `Build failed (attempt ${attempt}/${MAX_BUILD_RETRIES}): ${msg}. Retrying in ${delay / 1e3}s...`, "error");
28227
- await new Promise((resolve3) => setTimeout(resolve3, delay));
28506
+ await new Promise((resolve4) => setTimeout(resolve4, delay));
28228
28507
  return attemptBuild(attempt + 1);
28229
28508
  }
28230
28509
  serverLog("semantic", `Embeddings build failed after ${MAX_BUILD_RETRIES} attempts: ${msg}`, "error");
@@ -28280,8 +28559,8 @@ async function runPostIndexWork(ctx) {
28280
28559
  }
28281
28560
  } catch {
28282
28561
  try {
28283
- const dir = path39.dirname(rawPath);
28284
- const base = path39.basename(rawPath);
28562
+ const dir = path40.dirname(rawPath);
28563
+ const base = path40.basename(rawPath);
28285
28564
  const resolvedDir = realpathSync(dir).replace(/\\/g, "/");
28286
28565
  for (const prefix of vaultPrefixes) {
28287
28566
  if (resolvedDir.startsWith(prefix + "/") || resolvedDir === prefix) {
@@ -28313,7 +28592,7 @@ async function runPostIndexWork(ctx) {
28313
28592
  continue;
28314
28593
  }
28315
28594
  try {
28316
- const content = await fs35.readFile(path39.join(vp, event.path), "utf-8");
28595
+ const content = await fs35.readFile(path40.join(vp, event.path), "utf-8");
28317
28596
  const hash = createHash4("sha256").update(content).digest("hex").slice(0, 16);
28318
28597
  if (lastContentHashes.get(event.path) === hash) {
28319
28598
  serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
@@ -28400,7 +28679,8 @@ async function runPostIndexWork(ctx) {
28400
28679
  updateEntitiesInStateDb,
28401
28680
  getVaultIndex: () => vaultIndex,
28402
28681
  buildVaultIndex,
28403
- deferredScheduler: deferredScheduler ?? void 0
28682
+ deferredScheduler: deferredScheduler ?? void 0,
28683
+ runIntegrityCheck
28404
28684
  });
28405
28685
  await runner.run();
28406
28686
  };