@velvetmonkey/flywheel-memory 2.0.139 → 2.0.140

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +592 -511
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -4595,6 +4595,8 @@ __export(proactiveQueue_exports, {
4595
4595
  enqueueProactiveSuggestions: () => enqueueProactiveSuggestions,
4596
4596
  expireStaleEntries: () => expireStaleEntries
4597
4597
  });
4598
+ import * as path12 from "path";
4599
+ import { statSync as statSync3 } from "fs";
4598
4600
  function enqueueProactiveSuggestions(stateDb2, entries) {
4599
4601
  if (entries.length === 0) return 0;
4600
4602
  const now = Date.now();
@@ -4620,7 +4622,7 @@ function enqueueProactiveSuggestions(stateDb2, entries) {
4620
4622
  }
4621
4623
  return enqueued;
4622
4624
  }
4623
- async function drainProactiveQueue(stateDb2, vaultPath2, currentBatchPaths, config, applyFn) {
4625
+ async function drainProactiveQueue(stateDb2, vaultPath2, config, applyFn) {
4624
4626
  const result = {
4625
4627
  applied: [],
4626
4628
  expired: 0,
@@ -4652,7 +4654,14 @@ async function drainProactiveQueue(stateDb2, vaultPath2, currentBatchPaths, conf
4652
4654
  `SELECT COUNT(*) as cnt FROM wikilink_applications WHERE note_path = ? AND applied_at >= ?`
4653
4655
  );
4654
4656
  for (const [filePath, suggestions] of byFile) {
4655
- if (currentBatchPaths.has(filePath)) {
4657
+ const fullPath = path12.join(vaultPath2, filePath);
4658
+ try {
4659
+ const mtime = statSync3(fullPath).mtimeMs;
4660
+ if (Date.now() - mtime < MTIME_GUARD_MS) {
4661
+ result.skippedActiveEdit += suggestions.length;
4662
+ continue;
4663
+ }
4664
+ } catch {
4656
4665
  result.skippedActiveEdit += suggestions.length;
4657
4666
  continue;
4658
4667
  }
@@ -4698,12 +4707,13 @@ function expireStaleEntries(stateDb2) {
4698
4707
  ).run(Date.now());
4699
4708
  return result.changes;
4700
4709
  }
4701
- var QUEUE_TTL_MS;
4710
+ var QUEUE_TTL_MS, MTIME_GUARD_MS;
4702
4711
  var init_proactiveQueue = __esm({
4703
4712
  "src/core/write/proactiveQueue.ts"() {
4704
4713
  "use strict";
4705
4714
  init_serverLog();
4706
- QUEUE_TTL_MS = 60 * 60 * 1e3;
4715
+ QUEUE_TTL_MS = 6 * 60 * 60 * 1e3;
4716
+ MTIME_GUARD_MS = 6e4;
4707
4717
  }
4708
4718
  });
4709
4719
 
@@ -4722,7 +4732,7 @@ var init_constants = __esm({
4722
4732
 
4723
4733
  // src/core/write/writer.ts
4724
4734
  import fs21 from "fs/promises";
4725
- import path22 from "path";
4735
+ import path23 from "path";
4726
4736
  import matter5 from "gray-matter";
4727
4737
  import { createHash as createHash2 } from "node:crypto";
4728
4738
  function isSensitivePath(filePath) {
@@ -5050,13 +5060,13 @@ function validatePath(vaultPath2, notePath) {
5050
5060
  if (notePath.startsWith("\\")) {
5051
5061
  return false;
5052
5062
  }
5053
- const resolvedVault = path22.resolve(vaultPath2);
5054
- const resolvedNote = path22.resolve(vaultPath2, notePath);
5063
+ const resolvedVault = path23.resolve(vaultPath2);
5064
+ const resolvedNote = path23.resolve(vaultPath2, notePath);
5055
5065
  return resolvedNote.startsWith(resolvedVault);
5056
5066
  }
5057
5067
  function sanitizeNotePath(notePath) {
5058
- const dir = path22.dirname(notePath);
5059
- let filename = path22.basename(notePath);
5068
+ const dir = path23.dirname(notePath);
5069
+ let filename = path23.basename(notePath);
5060
5070
  const ext = filename.endsWith(".md") ? ".md" : "";
5061
5071
  let stem2 = ext ? filename.slice(0, -ext.length) : filename;
5062
5072
  stem2 = stem2.replace(/\s+/g, "-");
@@ -5065,7 +5075,7 @@ function sanitizeNotePath(notePath) {
5065
5075
  stem2 = stem2.replace(/-{2,}/g, "-");
5066
5076
  stem2 = stem2.replace(/^-+|-+$/g, "");
5067
5077
  filename = stem2 + (ext || ".md");
5068
- return dir === "." ? filename : path22.join(dir, filename).replace(/\\/g, "/");
5078
+ return dir === "." ? filename : path23.join(dir, filename).replace(/\\/g, "/");
5069
5079
  }
5070
5080
  async function validatePathSecure(vaultPath2, notePath) {
5071
5081
  if (notePath.startsWith("/")) {
@@ -5092,8 +5102,8 @@ async function validatePathSecure(vaultPath2, notePath) {
5092
5102
  reason: "Path traversal not allowed"
5093
5103
  };
5094
5104
  }
5095
- const resolvedVault = path22.resolve(vaultPath2);
5096
- const resolvedNote = path22.resolve(vaultPath2, notePath);
5105
+ const resolvedVault = path23.resolve(vaultPath2);
5106
+ const resolvedNote = path23.resolve(vaultPath2, notePath);
5097
5107
  if (!resolvedNote.startsWith(resolvedVault)) {
5098
5108
  return {
5099
5109
  valid: false,
@@ -5107,7 +5117,7 @@ async function validatePathSecure(vaultPath2, notePath) {
5107
5117
  };
5108
5118
  }
5109
5119
  try {
5110
- const fullPath = path22.join(vaultPath2, notePath);
5120
+ const fullPath = path23.join(vaultPath2, notePath);
5111
5121
  try {
5112
5122
  await fs21.access(fullPath);
5113
5123
  const realPath = await fs21.realpath(fullPath);
@@ -5118,7 +5128,7 @@ async function validatePathSecure(vaultPath2, notePath) {
5118
5128
  reason: "Symlink target is outside vault"
5119
5129
  };
5120
5130
  }
5121
- const relativePath = path22.relative(realVaultPath, realPath);
5131
+ const relativePath = path23.relative(realVaultPath, realPath);
5122
5132
  if (isSensitivePath(relativePath)) {
5123
5133
  return {
5124
5134
  valid: false,
@@ -5126,7 +5136,7 @@ async function validatePathSecure(vaultPath2, notePath) {
5126
5136
  };
5127
5137
  }
5128
5138
  } catch {
5129
- const parentDir = path22.dirname(fullPath);
5139
+ const parentDir = path23.dirname(fullPath);
5130
5140
  try {
5131
5141
  await fs21.access(parentDir);
5132
5142
  const realParentPath = await fs21.realpath(parentDir);
@@ -5155,7 +5165,7 @@ async function readVaultFile(vaultPath2, notePath) {
5155
5165
  if (!validatePath(vaultPath2, notePath)) {
5156
5166
  throw new Error("Invalid path: path traversal not allowed");
5157
5167
  }
5158
- const fullPath = path22.join(vaultPath2, notePath);
5168
+ const fullPath = path23.join(vaultPath2, notePath);
5159
5169
  const [rawContent, stat5] = await Promise.all([
5160
5170
  fs21.readFile(fullPath, "utf-8"),
5161
5171
  fs21.stat(fullPath)
@@ -5210,7 +5220,7 @@ async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEn
5210
5220
  if (!validation.valid) {
5211
5221
  throw new Error(`Invalid path: ${validation.reason}`);
5212
5222
  }
5213
- const fullPath = path22.join(vaultPath2, notePath);
5223
+ const fullPath = path23.join(vaultPath2, notePath);
5214
5224
  if (expectedHash) {
5215
5225
  const currentRaw = await fs21.readFile(fullPath, "utf-8");
5216
5226
  const currentHash = computeContentHash(currentRaw);
@@ -5610,8 +5620,8 @@ function createContext(variables = {}) {
5610
5620
  steps: {}
5611
5621
  };
5612
5622
  }
5613
- function resolvePath(obj, path36) {
5614
- const parts = path36.split(".");
5623
+ function resolvePath(obj, path37) {
5624
+ const parts = path37.split(".");
5615
5625
  let current = obj;
5616
5626
  for (const part of parts) {
5617
5627
  if (current === void 0 || current === null) {
@@ -6069,7 +6079,7 @@ __export(conditions_exports, {
6069
6079
  shouldStepExecute: () => shouldStepExecute
6070
6080
  });
6071
6081
  import fs28 from "fs/promises";
6072
- import path28 from "path";
6082
+ import path29 from "path";
6073
6083
  async function evaluateCondition(condition, vaultPath2, context) {
6074
6084
  const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
6075
6085
  const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
@@ -6122,7 +6132,7 @@ async function evaluateCondition(condition, vaultPath2, context) {
6122
6132
  }
6123
6133
  }
6124
6134
  async function evaluateFileExists(vaultPath2, notePath, expectExists) {
6125
- const fullPath = path28.join(vaultPath2, notePath);
6135
+ const fullPath = path29.join(vaultPath2, notePath);
6126
6136
  try {
6127
6137
  await fs28.access(fullPath);
6128
6138
  return {
@@ -6137,7 +6147,7 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
6137
6147
  }
6138
6148
  }
6139
6149
  async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
6140
- const fullPath = path28.join(vaultPath2, notePath);
6150
+ const fullPath = path29.join(vaultPath2, notePath);
6141
6151
  try {
6142
6152
  await fs28.access(fullPath);
6143
6153
  } catch {
@@ -6168,7 +6178,7 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
6168
6178
  }
6169
6179
  }
6170
6180
  async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
6171
- const fullPath = path28.join(vaultPath2, notePath);
6181
+ const fullPath = path29.join(vaultPath2, notePath);
6172
6182
  try {
6173
6183
  await fs28.access(fullPath);
6174
6184
  } catch {
@@ -6199,7 +6209,7 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
6199
6209
  }
6200
6210
  }
6201
6211
  async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
6202
- const fullPath = path28.join(vaultPath2, notePath);
6212
+ const fullPath = path29.join(vaultPath2, notePath);
6203
6213
  try {
6204
6214
  await fs28.access(fullPath);
6205
6215
  } catch {
@@ -6343,10 +6353,10 @@ var init_taskHelpers = __esm({
6343
6353
  });
6344
6354
 
6345
6355
  // src/index.ts
6346
- import * as path35 from "path";
6356
+ import * as path36 from "path";
6347
6357
  import { readFileSync as readFileSync6, realpathSync, existsSync as existsSync3 } from "fs";
6348
6358
  import { fileURLToPath as fileURLToPath2 } from "url";
6349
- import { dirname as dirname7, join as join20 } from "path";
6359
+ import { dirname as dirname7, join as join21 } from "path";
6350
6360
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6351
6361
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6352
6362
 
@@ -6581,8 +6591,8 @@ function updateIndexProgress(parsed, total) {
6581
6591
  function normalizeTarget(target) {
6582
6592
  return target.toLowerCase().replace(/\.md$/, "");
6583
6593
  }
6584
- function normalizeNotePath(path36) {
6585
- return path36.toLowerCase().replace(/\.md$/, "");
6594
+ function normalizeNotePath(path37) {
6595
+ return path37.toLowerCase().replace(/\.md$/, "");
6586
6596
  }
6587
6597
  async function buildVaultIndex(vaultPath2, options = {}) {
6588
6598
  const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
@@ -6751,7 +6761,7 @@ function findSimilarEntity(index, target) {
6751
6761
  }
6752
6762
  const maxDist = normalizedLen <= 10 ? 1 : 2;
6753
6763
  let bestMatch;
6754
- for (const [entity, path36] of index.entities) {
6764
+ for (const [entity, path37] of index.entities) {
6755
6765
  const lenDiff = Math.abs(entity.length - normalizedLen);
6756
6766
  if (lenDiff > maxDist) {
6757
6767
  continue;
@@ -6759,7 +6769,7 @@ function findSimilarEntity(index, target) {
6759
6769
  const dist = levenshteinDistance(normalized, entity);
6760
6770
  if (dist > 0 && dist <= maxDist) {
6761
6771
  if (!bestMatch || dist < bestMatch.distance) {
6762
- bestMatch = { path: path36, entity, distance: dist };
6772
+ bestMatch = { path: path37, entity, distance: dist };
6763
6773
  if (dist === 1) {
6764
6774
  return bestMatch;
6765
6775
  }
@@ -7290,30 +7300,30 @@ var EventQueue = class {
7290
7300
  * Add a new event to the queue
7291
7301
  */
7292
7302
  push(type, rawPath) {
7293
- const path36 = normalizePath(rawPath);
7303
+ const path37 = normalizePath(rawPath);
7294
7304
  const now = Date.now();
7295
7305
  const event = {
7296
7306
  type,
7297
- path: path36,
7307
+ path: path37,
7298
7308
  timestamp: now
7299
7309
  };
7300
- let pending = this.pending.get(path36);
7310
+ let pending = this.pending.get(path37);
7301
7311
  if (!pending) {
7302
7312
  pending = {
7303
7313
  events: [],
7304
7314
  timer: null,
7305
7315
  lastEvent: now
7306
7316
  };
7307
- this.pending.set(path36, pending);
7317
+ this.pending.set(path37, pending);
7308
7318
  }
7309
7319
  pending.events.push(event);
7310
7320
  pending.lastEvent = now;
7311
- console.error(`[flywheel] QUEUE: pushed ${type} for ${path36}, pending=${this.pending.size}`);
7321
+ console.error(`[flywheel] QUEUE: pushed ${type} for ${path37}, pending=${this.pending.size}`);
7312
7322
  if (pending.timer) {
7313
7323
  clearTimeout(pending.timer);
7314
7324
  }
7315
7325
  pending.timer = setTimeout(() => {
7316
- this.flushPath(path36);
7326
+ this.flushPath(path37);
7317
7327
  }, this.config.debounceMs);
7318
7328
  if (this.pending.size >= this.config.batchSize) {
7319
7329
  this.flush();
@@ -7334,10 +7344,10 @@ var EventQueue = class {
7334
7344
  /**
7335
7345
  * Flush a single path's events
7336
7346
  */
7337
- flushPath(path36) {
7338
- const pending = this.pending.get(path36);
7347
+ flushPath(path37) {
7348
+ const pending = this.pending.get(path37);
7339
7349
  if (!pending || pending.events.length === 0) return;
7340
- console.error(`[flywheel] QUEUE: flushing ${path36}, events=${pending.events.length}`);
7350
+ console.error(`[flywheel] QUEUE: flushing ${path37}, events=${pending.events.length}`);
7341
7351
  if (pending.timer) {
7342
7352
  clearTimeout(pending.timer);
7343
7353
  pending.timer = null;
@@ -7346,7 +7356,7 @@ var EventQueue = class {
7346
7356
  if (coalescedType) {
7347
7357
  const coalesced = {
7348
7358
  type: coalescedType,
7349
- path: path36,
7359
+ path: path37,
7350
7360
  originalEvents: [...pending.events]
7351
7361
  };
7352
7362
  this.onBatch({
@@ -7355,7 +7365,7 @@ var EventQueue = class {
7355
7365
  timestamp: Date.now()
7356
7366
  });
7357
7367
  }
7358
- this.pending.delete(path36);
7368
+ this.pending.delete(path37);
7359
7369
  }
7360
7370
  /**
7361
7371
  * Flush all pending events
@@ -7367,7 +7377,7 @@ var EventQueue = class {
7367
7377
  }
7368
7378
  if (this.pending.size === 0) return;
7369
7379
  const events = [];
7370
- for (const [path36, pending] of this.pending) {
7380
+ for (const [path37, pending] of this.pending) {
7371
7381
  if (pending.timer) {
7372
7382
  clearTimeout(pending.timer);
7373
7383
  }
@@ -7375,7 +7385,7 @@ var EventQueue = class {
7375
7385
  if (coalescedType) {
7376
7386
  events.push({
7377
7387
  type: coalescedType,
7378
- path: path36,
7388
+ path: path37,
7379
7389
  originalEvents: [...pending.events]
7380
7390
  });
7381
7391
  }
@@ -7761,31 +7771,31 @@ function createVaultWatcher(options) {
7761
7771
  usePolling: config.usePolling,
7762
7772
  interval: config.usePolling ? config.pollInterval : void 0
7763
7773
  });
7764
- watcher.on("add", (path36) => {
7765
- console.error(`[flywheel] RAW EVENT: add ${path36}`);
7766
- if (shouldWatch(path36, vaultPath2)) {
7767
- console.error(`[flywheel] ACCEPTED: add ${path36}`);
7768
- eventQueue.push("add", path36);
7774
+ watcher.on("add", (path37) => {
7775
+ console.error(`[flywheel] RAW EVENT: add ${path37}`);
7776
+ if (shouldWatch(path37, vaultPath2)) {
7777
+ console.error(`[flywheel] ACCEPTED: add ${path37}`);
7778
+ eventQueue.push("add", path37);
7769
7779
  } else {
7770
- console.error(`[flywheel] FILTERED: add ${path36}`);
7780
+ console.error(`[flywheel] FILTERED: add ${path37}`);
7771
7781
  }
7772
7782
  });
7773
- watcher.on("change", (path36) => {
7774
- console.error(`[flywheel] RAW EVENT: change ${path36}`);
7775
- if (shouldWatch(path36, vaultPath2)) {
7776
- console.error(`[flywheel] ACCEPTED: change ${path36}`);
7777
- eventQueue.push("change", path36);
7783
+ watcher.on("change", (path37) => {
7784
+ console.error(`[flywheel] RAW EVENT: change ${path37}`);
7785
+ if (shouldWatch(path37, vaultPath2)) {
7786
+ console.error(`[flywheel] ACCEPTED: change ${path37}`);
7787
+ eventQueue.push("change", path37);
7778
7788
  } else {
7779
- console.error(`[flywheel] FILTERED: change ${path36}`);
7789
+ console.error(`[flywheel] FILTERED: change ${path37}`);
7780
7790
  }
7781
7791
  });
7782
- watcher.on("unlink", (path36) => {
7783
- console.error(`[flywheel] RAW EVENT: unlink ${path36}`);
7784
- if (shouldWatch(path36, vaultPath2)) {
7785
- console.error(`[flywheel] ACCEPTED: unlink ${path36}`);
7786
- eventQueue.push("unlink", path36);
7792
+ watcher.on("unlink", (path37) => {
7793
+ console.error(`[flywheel] RAW EVENT: unlink ${path37}`);
7794
+ if (shouldWatch(path37, vaultPath2)) {
7795
+ console.error(`[flywheel] ACCEPTED: unlink ${path37}`);
7796
+ eventQueue.push("unlink", path37);
7787
7797
  } else {
7788
- console.error(`[flywheel] FILTERED: unlink ${path36}`);
7798
+ console.error(`[flywheel] FILTERED: unlink ${path37}`);
7789
7799
  }
7790
7800
  });
7791
7801
  watcher.on("ready", () => {
@@ -7817,8 +7827,8 @@ function createVaultWatcher(options) {
7817
7827
  }
7818
7828
 
7819
7829
  // src/core/read/watch/pipeline.ts
7820
- import * as path14 from "node:path";
7821
- import * as fs9 from "node:fs/promises";
7830
+ import * as path15 from "node:path";
7831
+ import * as fs10 from "node:fs/promises";
7822
7832
  import {
7823
7833
  getAllEntitiesFromDb,
7824
7834
  findEntityMatches,
@@ -8071,14 +8081,283 @@ init_cooccurrence();
8071
8081
  init_wikilinks();
8072
8082
  init_proactiveQueue();
8073
8083
  init_retrievalCooccurrence();
8084
+
8085
+ // src/core/read/fts5.ts
8086
+ init_vault();
8087
+ init_serverLog();
8088
+ init_vault_scope();
8089
+ import * as fs8 from "fs";
8090
+ var EXCLUDED_DIRS3 = /* @__PURE__ */ new Set([
8091
+ ".obsidian",
8092
+ ".trash",
8093
+ ".git",
8094
+ "node_modules",
8095
+ "templates",
8096
+ ".claude",
8097
+ ".flywheel"
8098
+ ]);
8099
+ var MAX_INDEX_FILE_SIZE = 5 * 1024 * 1024;
8100
+ var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
8101
+ function splitFrontmatter(raw) {
8102
+ if (!raw.startsWith("---")) return { frontmatter: "", body: raw };
8103
+ const end = raw.indexOf("\n---", 3);
8104
+ if (end === -1) return { frontmatter: "", body: raw };
8105
+ const yaml = raw.substring(4, end);
8106
+ const values = yaml.split("\n").map((line) => line.replace(/^[\s-]*/, "").replace(/^[\w]+:\s*/, "")).filter((v) => v && !v.startsWith("[") && !v.startsWith("{")).join(" ");
8107
+ return { frontmatter: values, body: raw.substring(end + 4) };
8108
+ }
8109
+ var db2 = null;
8110
+ function getDb2() {
8111
+ return getActiveScopeOrNull()?.stateDb?.db ?? db2;
8112
+ }
8113
+ var state = {
8114
+ ready: false,
8115
+ building: false,
8116
+ lastBuilt: null,
8117
+ noteCount: 0,
8118
+ error: null
8119
+ };
8120
+ function setFTS5Database(database) {
8121
+ db2 = database;
8122
+ try {
8123
+ const row = db2.prepare(
8124
+ "SELECT value FROM fts_metadata WHERE key = ?"
8125
+ ).get("last_built");
8126
+ if (row) {
8127
+ const lastBuilt = new Date(row.value);
8128
+ const countRow = db2.prepare("SELECT COUNT(*) as count FROM notes_fts").get();
8129
+ state = {
8130
+ ready: countRow.count > 0,
8131
+ building: false,
8132
+ lastBuilt,
8133
+ noteCount: countRow.count,
8134
+ error: null
8135
+ };
8136
+ }
8137
+ } catch {
8138
+ }
8139
+ }
8140
+ function shouldIndexFile2(filePath) {
8141
+ const parts = filePath.split("/");
8142
+ return !parts.some((part) => EXCLUDED_DIRS3.has(part));
8143
+ }
8144
+ async function buildFTS5Index(vaultPath2) {
8145
+ const db4 = getDb2();
8146
+ try {
8147
+ state.error = null;
8148
+ state.building = true;
8149
+ if (!db4) {
8150
+ throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
8151
+ }
8152
+ const files = await scanVault(vaultPath2);
8153
+ const indexableFiles = files.filter((f) => shouldIndexFile2(f.path));
8154
+ const rows = [];
8155
+ for (const file of indexableFiles) {
8156
+ try {
8157
+ const stats = fs8.statSync(file.absolutePath);
8158
+ if (stats.size > MAX_INDEX_FILE_SIZE) {
8159
+ continue;
8160
+ }
8161
+ const raw = fs8.readFileSync(file.absolutePath, "utf-8");
8162
+ const { frontmatter, body } = splitFrontmatter(raw);
8163
+ const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
8164
+ rows.push([file.path, title, frontmatter, body]);
8165
+ } catch (err) {
8166
+ serverLog("fts5", `Skipping ${file.path}: ${err}`, "warn");
8167
+ }
8168
+ }
8169
+ const insert = db4.prepare(
8170
+ "INSERT INTO notes_fts (path, title, frontmatter, content) VALUES (?, ?, ?, ?)"
8171
+ );
8172
+ const now = /* @__PURE__ */ new Date();
8173
+ const swapAll = db4.transaction(() => {
8174
+ db4.exec("DELETE FROM notes_fts");
8175
+ for (const row of rows) {
8176
+ insert.run(...row);
8177
+ }
8178
+ db4.prepare(
8179
+ "INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
8180
+ ).run("last_built", now.toISOString());
8181
+ });
8182
+ swapAll();
8183
+ const indexed = rows.length;
8184
+ state = {
8185
+ ready: true,
8186
+ building: false,
8187
+ lastBuilt: now,
8188
+ noteCount: indexed,
8189
+ error: null
8190
+ };
8191
+ serverLog("fts5", `Indexed ${indexed} notes`);
8192
+ return state;
8193
+ } catch (err) {
8194
+ state = {
8195
+ ready: false,
8196
+ building: false,
8197
+ lastBuilt: null,
8198
+ noteCount: 0,
8199
+ error: err instanceof Error ? err.message : String(err)
8200
+ };
8201
+ throw err;
8202
+ }
8203
+ }
8204
+ function isIndexStale(_vaultPath) {
8205
+ const db4 = getDb2();
8206
+ if (!db4) {
8207
+ return true;
8208
+ }
8209
+ try {
8210
+ const row = db4.prepare(
8211
+ "SELECT value FROM fts_metadata WHERE key = ?"
8212
+ ).get("last_built");
8213
+ if (!row) {
8214
+ return true;
8215
+ }
8216
+ const lastBuilt = new Date(row.value);
8217
+ const age = Date.now() - lastBuilt.getTime();
8218
+ return age > STALE_THRESHOLD_MS;
8219
+ } catch {
8220
+ return true;
8221
+ }
8222
+ }
8223
+ function updateFTS5Incremental(vaultPath2, changed, deleted) {
8224
+ const db4 = getDb2();
8225
+ if (!db4 || !state.ready) return { updated: 0, removed: 0 };
8226
+ const del = db4.prepare("DELETE FROM notes_fts WHERE path = ?");
8227
+ const ins = db4.prepare(
8228
+ "INSERT INTO notes_fts (path, title, frontmatter, content) VALUES (?, ?, ?, ?)"
8229
+ );
8230
+ let updated = 0;
8231
+ let removed = 0;
8232
+ const run = db4.transaction(() => {
8233
+ for (const p of deleted) {
8234
+ del.run(p);
8235
+ removed++;
8236
+ }
8237
+ for (const p of changed) {
8238
+ if (!shouldIndexFile2(p)) continue;
8239
+ const absPath = `${vaultPath2}/${p}`.replace(/\\/g, "/");
8240
+ try {
8241
+ const stats = fs8.statSync(absPath);
8242
+ if (stats.size > MAX_INDEX_FILE_SIZE) continue;
8243
+ const raw = fs8.readFileSync(absPath, "utf-8");
8244
+ const { frontmatter, body } = splitFrontmatter(raw);
8245
+ const title = p.replace(/\.md$/, "").split("/").pop() || p;
8246
+ del.run(p);
8247
+ ins.run(p, title, frontmatter, body);
8248
+ updated++;
8249
+ } catch {
8250
+ del.run(p);
8251
+ }
8252
+ }
8253
+ });
8254
+ run();
8255
+ if (updated > 0 || removed > 0) {
8256
+ state.noteCount = db4.prepare("SELECT COUNT(*) as count FROM notes_fts").get().count;
8257
+ }
8258
+ return { updated, removed };
8259
+ }
8260
+ function sanitizeFTS5Query(query) {
8261
+ if (!query?.trim()) return "";
8262
+ const phrases = [];
8263
+ const withoutPhrases = query.replace(/"([^"]+)"/g, (_, phrase) => {
8264
+ phrases.push(`"${phrase.replace(/"/g, '""')}"`);
8265
+ return "";
8266
+ });
8267
+ const cleaned = withoutPhrases.replace(/[(){}[\]^~:\-]/g, " ").replace(/\s+/g, " ").trim();
8268
+ const tokens = cleaned.split(" ").filter((t) => t && t !== "AND" && t !== "OR" && t !== "NOT");
8269
+ const parts = [...phrases];
8270
+ if (tokens.length === 1) {
8271
+ parts.push(tokens[0]);
8272
+ } else if (tokens.length > 1) {
8273
+ parts.push(tokens.join(" OR "));
8274
+ }
8275
+ return parts.join(" ") || "";
8276
+ }
8277
+ function searchFTS5(_vaultPath, query, limit = 10) {
8278
+ const db4 = getDb2();
8279
+ if (!db4) {
8280
+ throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
8281
+ }
8282
+ const sanitized = sanitizeFTS5Query(query);
8283
+ if (!sanitized) return [];
8284
+ try {
8285
+ const stmt = db4.prepare(`
8286
+ SELECT
8287
+ path,
8288
+ title,
8289
+ snippet(notes_fts, 3, '<mark>', '</mark>', '...', 64) as snippet
8290
+ FROM notes_fts
8291
+ WHERE notes_fts MATCH ?
8292
+ ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
8293
+ LIMIT ?
8294
+ `);
8295
+ const results = stmt.all(sanitized, limit);
8296
+ return results;
8297
+ } catch (err) {
8298
+ if (err instanceof Error && err.message.includes("fts5:")) {
8299
+ return [];
8300
+ }
8301
+ throw err;
8302
+ }
8303
+ }
8304
+ function getFTS5State() {
8305
+ if (state.building) return { ...state };
8306
+ const scopeDb = getDb2();
8307
+ if (scopeDb) {
8308
+ try {
8309
+ const row = scopeDb.prepare(
8310
+ "SELECT value FROM fts_metadata WHERE key = ?"
8311
+ ).get("last_built");
8312
+ const countRow = scopeDb.prepare("SELECT COUNT(*) as count FROM notes_fts").get();
8313
+ return {
8314
+ ready: countRow.count > 0,
8315
+ building: false,
8316
+ lastBuilt: row ? new Date(row.value) : null,
8317
+ noteCount: countRow.count,
8318
+ error: null
8319
+ };
8320
+ } catch {
8321
+ }
8322
+ }
8323
+ return { ...state };
8324
+ }
8325
+ function getContentPreview(notePath, maxChars = 300) {
8326
+ const db4 = getDb2();
8327
+ if (!db4) return null;
8328
+ try {
8329
+ const row = db4.prepare(
8330
+ "SELECT substr(content, 1, ?) as preview FROM notes_fts WHERE path = ?"
8331
+ ).get(maxChars + 50, notePath);
8332
+ if (!row?.preview) return null;
8333
+ const truncated = row.preview.length > maxChars ? row.preview.slice(0, maxChars).replace(/\s\S*$/, "") + "..." : row.preview;
8334
+ return truncated;
8335
+ } catch {
8336
+ return null;
8337
+ }
8338
+ }
8339
+ function countFTS5Mentions(term) {
8340
+ const db4 = getDb2();
8341
+ if (!db4) return 0;
8342
+ try {
8343
+ const result = db4.prepare(
8344
+ "SELECT COUNT(*) as cnt FROM notes_fts WHERE content MATCH ?"
8345
+ ).get(`"${term}"`);
8346
+ return result?.cnt ?? 0;
8347
+ } catch {
8348
+ return 0;
8349
+ }
8350
+ }
8351
+
8352
+ // src/core/read/watch/pipeline.ts
8074
8353
  init_embeddings();
8075
8354
 
8076
8355
  // src/core/read/taskCache.ts
8077
- import * as path13 from "path";
8356
+ import * as path14 from "path";
8078
8357
 
8079
8358
  // src/tools/read/tasks.ts
8080
- import * as fs8 from "fs";
8081
- import * as path12 from "path";
8359
+ import * as fs9 from "fs";
8360
+ import * as path13 from "path";
8082
8361
  var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
8083
8362
  var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
8084
8363
  var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
@@ -8104,7 +8383,7 @@ function extractDueDate(text) {
8104
8383
  async function extractTasksFromNote(notePath, absolutePath) {
8105
8384
  let content;
8106
8385
  try {
8107
- content = await fs8.promises.readFile(absolutePath, "utf-8");
8386
+ content = await fs9.promises.readFile(absolutePath, "utf-8");
8108
8387
  } catch {
8109
8388
  return [];
8110
8389
  }
@@ -8147,7 +8426,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
8147
8426
  const allTasks = [];
8148
8427
  for (const note of index.notes.values()) {
8149
8428
  if (folder && !note.path.startsWith(folder)) continue;
8150
- const absolutePath = path12.join(vaultPath2, note.path);
8429
+ const absolutePath = path13.join(vaultPath2, note.path);
8151
8430
  const tasks = await extractTasksFromNote(note.path, absolutePath);
8152
8431
  allTasks.push(...tasks);
8153
8432
  }
@@ -8191,7 +8470,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
8191
8470
  async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
8192
8471
  const note = index.notes.get(notePath);
8193
8472
  if (!note) return null;
8194
- const absolutePath = path12.join(vaultPath2, notePath);
8473
+ const absolutePath = path13.join(vaultPath2, notePath);
8195
8474
  let tasks = await extractTasksFromNote(notePath, absolutePath);
8196
8475
  if (excludeTags.length > 0) {
8197
8476
  tasks = tasks.filter(
@@ -8213,17 +8492,17 @@ async function getTasksWithDueDates(index, vaultPath2, options = {}) {
8213
8492
  // src/core/read/taskCache.ts
8214
8493
  init_serverLog();
8215
8494
  init_vault_scope();
8216
- var db2 = null;
8217
- function getDb2() {
8218
- return getActiveScopeOrNull()?.stateDb?.db ?? db2;
8495
+ var db3 = null;
8496
+ function getDb3() {
8497
+ return getActiveScopeOrNull()?.stateDb?.db ?? db3;
8219
8498
  }
8220
8499
  var TASK_CACHE_STALE_MS = 30 * 60 * 1e3;
8221
8500
  var cacheReady = false;
8222
8501
  var rebuildInProgress = false;
8223
8502
  function setTaskCacheDatabase(database) {
8224
- db2 = database;
8503
+ db3 = database;
8225
8504
  try {
8226
- const row = db2.prepare(
8505
+ const row = db3.prepare(
8227
8506
  "SELECT value FROM fts_metadata WHERE key = ?"
8228
8507
  ).get("task_cache_built");
8229
8508
  if (row) {
@@ -8233,7 +8512,7 @@ function setTaskCacheDatabase(database) {
8233
8512
  }
8234
8513
  }
8235
8514
  function isTaskCacheReady() {
8236
- const scopeDb = getDb2();
8515
+ const scopeDb = getDb3();
8237
8516
  if (!scopeDb) return false;
8238
8517
  try {
8239
8518
  const row = scopeDb.prepare(
@@ -8248,7 +8527,7 @@ function isTaskCacheBuilding() {
8248
8527
  return rebuildInProgress;
8249
8528
  }
8250
8529
  async function buildTaskCache(vaultPath2, index, excludeTags) {
8251
- const db4 = getDb2();
8530
+ const db4 = getDb3();
8252
8531
  if (!db4) {
8253
8532
  throw new Error("Task cache database not initialized. Call setTaskCacheDatabase() first.");
8254
8533
  }
@@ -8262,7 +8541,7 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
8262
8541
  }
8263
8542
  const allRows = [];
8264
8543
  for (const notePath of notePaths) {
8265
- const absolutePath = path13.join(vaultPath2, notePath);
8544
+ const absolutePath = path14.join(vaultPath2, notePath);
8266
8545
  const tasks = await extractTasksFromNote(notePath, absolutePath);
8267
8546
  for (const task of tasks) {
8268
8547
  if (excludeTags?.length && excludeTags.some((t) => task.tags.includes(t))) {
@@ -8302,10 +8581,10 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
8302
8581
  }
8303
8582
  }
8304
8583
  async function updateTaskCacheForFile(vaultPath2, relativePath) {
8305
- const db4 = getDb2();
8584
+ const db4 = getDb3();
8306
8585
  if (!db4) return;
8307
8586
  db4.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
8308
- const absolutePath = path13.join(vaultPath2, relativePath);
8587
+ const absolutePath = path14.join(vaultPath2, relativePath);
8309
8588
  const tasks = await extractTasksFromNote(relativePath, absolutePath);
8310
8589
  if (tasks.length > 0) {
8311
8590
  const insertStmt = db4.prepare(`
@@ -8330,12 +8609,12 @@ async function updateTaskCacheForFile(vaultPath2, relativePath) {
8330
8609
  }
8331
8610
  }
8332
8611
  function removeTaskCacheForFile(relativePath) {
8333
- const db4 = getDb2();
8612
+ const db4 = getDb3();
8334
8613
  if (!db4) return;
8335
8614
  db4.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
8336
8615
  }
8337
8616
  function queryTasksFromCache(options) {
8338
- const db4 = getDb2();
8617
+ const db4 = getDb3();
8339
8618
  if (!db4) {
8340
8619
  throw new Error("Task cache database not initialized.");
8341
8620
  }
@@ -8422,7 +8701,7 @@ function queryTasksFromCache(options) {
8422
8701
  };
8423
8702
  }
8424
8703
  function isTaskCacheStale() {
8425
- const db4 = getDb2();
8704
+ const db4 = getDb3();
8426
8705
  if (!db4) return true;
8427
8706
  try {
8428
8707
  const row = db4.prepare(
@@ -8478,6 +8757,7 @@ var PipelineRunner = class {
8478
8757
  try {
8479
8758
  await runStep("drain_proactive_queue", tracker, {}, () => this.drainQueue());
8480
8759
  await this.indexRebuild();
8760
+ this.fts5Incremental();
8481
8761
  this.noteMoves();
8482
8762
  await this.entityScan();
8483
8763
  await runStep("hub_scores", tracker, { entity_count: this.entitiesAfter.length }, () => this.hubScores());
@@ -8540,7 +8820,7 @@ var PipelineRunner = class {
8540
8820
  } else {
8541
8821
  const absoluteBatch = {
8542
8822
  ...p.batch,
8543
- events: p.events.map((e) => ({ ...e, path: path14.join(p.vp, e.path) }))
8823
+ events: p.events.map((e) => ({ ...e, path: path15.join(p.vp, e.path) }))
8544
8824
  };
8545
8825
  const batchResult = await processBatch(vaultIndex2, p.vp, absoluteBatch);
8546
8826
  serverLog("watcher", `Incremental: ${batchResult.successful}/${batchResult.total} files in ${batchResult.durationMs}ms`);
@@ -8549,6 +8829,26 @@ var PipelineRunner = class {
8549
8829
  const idx = p.getVaultIndex();
8550
8830
  tracker.end({ note_count: idx.notes.size, entity_count: idx.entities.size, tag_count: idx.tags.size });
8551
8831
  }
8832
+ // ── Step 1.1: FTS5 incremental update ──────────────────────────────
8833
+ fts5Incremental() {
8834
+ const { p, tracker } = this;
8835
+ const changed = p.events.filter((e) => e.type === "upsert").map((e) => e.path);
8836
+ const deleted = [
8837
+ ...p.events.filter((e) => e.type === "delete").map((e) => e.path),
8838
+ ...p.renames.map((r) => r.oldPath)
8839
+ ];
8840
+ if (changed.length === 0 && deleted.length === 0) {
8841
+ tracker.start("fts5_incremental", {});
8842
+ tracker.skip("fts5_incremental", "no changes");
8843
+ return;
8844
+ }
8845
+ tracker.start("fts5_incremental", { changed: changed.length, deleted: deleted.length });
8846
+ const result = updateFTS5Incremental(p.vp, changed, deleted);
8847
+ tracker.end(result);
8848
+ if (result.updated > 0 || result.removed > 0) {
8849
+ serverLog("watcher", `FTS5: ${result.updated} updated, ${result.removed} removed`);
8850
+ }
8851
+ }
8552
8852
  // ── Step 1.5: Note moves ──────────────────────────────────────────
8553
8853
  noteMoves() {
8554
8854
  const { p, tracker } = this;
@@ -8689,7 +8989,7 @@ var PipelineRunner = class {
8689
8989
  removeEmbedding(event.path);
8690
8990
  embRemoved++;
8691
8991
  } else if (event.path.endsWith(".md")) {
8692
- const absPath = path14.join(p.vp, event.path);
8992
+ const absPath = path15.join(p.vp, event.path);
8693
8993
  await updateEmbedding(event.path, absPath);
8694
8994
  embUpdated++;
8695
8995
  }
@@ -8923,7 +9223,7 @@ var PipelineRunner = class {
8923
9223
  for (const event of p.events) {
8924
9224
  if (event.type === "delete" || !event.path.endsWith(".md")) continue;
8925
9225
  try {
8926
- const content = await fs9.readFile(path14.join(p.vp, event.path), "utf-8");
9226
+ const content = await fs10.readFile(path15.join(p.vp, event.path), "utf-8");
8927
9227
  const zones = getProtectedZones(content);
8928
9228
  const linked = new Set(
8929
9229
  (this.forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).map((n) => n.toLowerCase())
@@ -8968,7 +9268,7 @@ var PipelineRunner = class {
8968
9268
  for (const event of p.events) {
8969
9269
  if (event.type === "delete" || !event.path.endsWith(".md")) continue;
8970
9270
  try {
8971
- const content = await fs9.readFile(path14.join(p.vp, event.path), "utf-8");
9271
+ const content = await fs10.readFile(path15.join(p.vp, event.path), "utf-8");
8972
9272
  const removed = processImplicitFeedback(p.sd, event.path, content);
8973
9273
  for (const entity of removed) feedbackResults.push({ entity, file: event.path });
8974
9274
  } catch {
@@ -9045,7 +9345,7 @@ var PipelineRunner = class {
9045
9345
  for (const event of p.events) {
9046
9346
  if (event.type === "delete" || !event.path.endsWith(".md")) continue;
9047
9347
  try {
9048
- const content = await fs9.readFile(path14.join(p.vp, event.path), "utf-8");
9348
+ const content = await fs10.readFile(path15.join(p.vp, event.path), "utf-8");
9049
9349
  const zones = getProtectedZones(content);
9050
9350
  const linkedSet = new Set(
9051
9351
  (this.forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).concat(this.forwardLinkResults.find((r) => r.file === event.path)?.dead ?? []).map((n) => n.toLowerCase())
@@ -9081,7 +9381,7 @@ var PipelineRunner = class {
9081
9381
  for (const event of p.events) {
9082
9382
  if (event.type === "delete" || !event.path.endsWith(".md")) continue;
9083
9383
  try {
9084
- const rawContent = await fs9.readFile(path14.join(p.vp, event.path), "utf-8");
9384
+ const rawContent = await fs10.readFile(path15.join(p.vp, event.path), "utf-8");
9085
9385
  const content = rawContent.replace(/ → \[\[.*$/gm, "");
9086
9386
  const result = await suggestRelatedLinks(content, {
9087
9387
  maxSuggestions: 5,
@@ -9112,14 +9412,12 @@ var PipelineRunner = class {
9112
9412
  if (!p.sd || p.flywheelConfig?.proactive_linking === false) {
9113
9413
  return { skipped: true };
9114
9414
  }
9115
- const currentBatchPaths = new Set(p.events.map((e) => e.path));
9116
9415
  const result = await drainProactiveQueue(
9117
9416
  p.sd,
9118
9417
  p.vp,
9119
- currentBatchPaths,
9120
9418
  {
9121
9419
  minScore: p.flywheelConfig?.proactive_min_score ?? 20,
9122
- maxPerFile: p.flywheelConfig?.proactive_max_per_file ?? 3,
9420
+ maxPerFile: p.flywheelConfig?.proactive_max_per_file ?? 5,
9123
9421
  maxPerDay: p.flywheelConfig?.proactive_max_per_day ?? 10
9124
9422
  },
9125
9423
  applyProactiveSuggestions
@@ -9145,7 +9443,7 @@ var PipelineRunner = class {
9145
9443
  tracker.start("proactive_enqueue", { files: this.suggestionResults.length });
9146
9444
  try {
9147
9445
  const minScore = p.flywheelConfig?.proactive_min_score ?? 20;
9148
- const maxPerFile = p.flywheelConfig?.proactive_max_per_file ?? 3;
9446
+ const maxPerFile = p.flywheelConfig?.proactive_max_per_file ?? 5;
9149
9447
  const entries = [];
9150
9448
  for (const { file, top } of this.suggestionResults) {
9151
9449
  const candidates = top.filter((s) => s.score >= minScore && s.confidence === "high").slice(0, maxPerFile);
@@ -9166,323 +9464,106 @@ var PipelineRunner = class {
9166
9464
  // ── Step 13: Tag scan ─────────────────────────────────────────────
9167
9465
  async tagScan() {
9168
9466
  const { p } = this;
9169
- const vaultIndex2 = p.getVaultIndex();
9170
- const tagDiffs = [];
9171
- if (p.sd) {
9172
- const noteTagsForward = /* @__PURE__ */ new Map();
9173
- for (const [tag, paths] of vaultIndex2.tags) {
9174
- for (const notePath of paths) {
9175
- if (!noteTagsForward.has(notePath)) noteTagsForward.set(notePath, /* @__PURE__ */ new Set());
9176
- noteTagsForward.get(notePath).add(tag);
9177
- }
9178
- }
9179
- for (const event of p.events) {
9180
- if (event.type === "delete" || !event.path.endsWith(".md")) continue;
9181
- const currentSet = noteTagsForward.get(event.path) ?? /* @__PURE__ */ new Set();
9182
- const previousSet = getStoredNoteTags(p.sd, event.path);
9183
- if (previousSet.size === 0 && currentSet.size > 0) {
9184
- updateStoredNoteTags(p.sd, event.path, currentSet);
9185
- continue;
9186
- }
9187
- const added = [...currentSet].filter((t) => !previousSet.has(t));
9188
- const removed = [...previousSet].filter((t) => !currentSet.has(t));
9189
- if (added.length > 0 || removed.length > 0) {
9190
- tagDiffs.push({ file: event.path, added, removed });
9191
- }
9192
- updateStoredNoteTags(p.sd, event.path, currentSet);
9193
- }
9194
- for (const event of p.events) {
9195
- if (event.type === "delete") {
9196
- const previousSet = getStoredNoteTags(p.sd, event.path);
9197
- if (previousSet.size > 0) {
9198
- tagDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
9199
- updateStoredNoteTags(p.sd, event.path, /* @__PURE__ */ new Set());
9200
- }
9201
- }
9202
- }
9203
- }
9204
- const totalTagsAdded = tagDiffs.reduce((s, d) => s + d.added.length, 0);
9205
- const totalTagsRemoved = tagDiffs.reduce((s, d) => s + d.removed.length, 0);
9206
- if (tagDiffs.length > 0) {
9207
- serverLog("watcher", `Tag scan: ${totalTagsAdded} added, ${totalTagsRemoved} removed across ${tagDiffs.length} files`);
9208
- }
9209
- return { total_added: totalTagsAdded, total_removed: totalTagsRemoved, tag_diffs: tagDiffs };
9210
- }
9211
- // ── Step 19: Retrieval co-occurrence ──────────────────────────────
9212
- async retrievalCooccurrence() {
9213
- const { p } = this;
9214
- if (!p.sd) return { skipped: "no sd" };
9215
- const inserted = mineRetrievalCooccurrence(p.sd);
9216
- if (inserted > 0) {
9217
- serverLog("watcher", `Retrieval co-occurrence: ${inserted} new pairs`);
9218
- }
9219
- return { pairs_inserted: inserted };
9220
- }
9221
- };
9222
-
9223
- // src/core/read/logging.ts
9224
- init_serverLog();
9225
- import {
9226
- createLoggerFromConfig,
9227
- generateSessionId,
9228
- setSessionId
9229
- } from "@velvetmonkey/vault-core";
9230
- var logger = null;
9231
- async function initializeLogger(vaultPath2) {
9232
- try {
9233
- const sessionId = generateSessionId();
9234
- setSessionId(sessionId);
9235
- logger = await createLoggerFromConfig(vaultPath2, "flywheel");
9236
- } catch (error) {
9237
- serverLog("server", `Failed to initialize logger: ${error}`, "error");
9238
- logger = null;
9239
- }
9240
- }
9241
- function getLogger() {
9242
- return logger;
9243
- }
9244
-
9245
- // src/index.ts
9246
- init_wikilinks();
9247
-
9248
- // src/core/write/logging.ts
9249
- init_serverLog();
9250
- import {
9251
- createLoggerFromConfig as createLoggerFromConfig2,
9252
- generateSessionId as generateSessionId2,
9253
- setSessionId as setSessionId2
9254
- } from "@velvetmonkey/vault-core";
9255
- var logger2 = null;
9256
- async function initializeLogger2(vaultPath2) {
9257
- try {
9258
- const sessionId = generateSessionId2();
9259
- setSessionId2(sessionId);
9260
- logger2 = await createLoggerFromConfig2(vaultPath2, "write");
9261
- } catch (error) {
9262
- serverLog("server", `Failed to initialize logger: ${error}`, "error");
9263
- logger2 = null;
9264
- }
9265
- }
9266
- async function flushLogs() {
9267
- if (logger2) await logger2.flush();
9268
- }
9269
-
9270
- // src/core/read/fts5.ts
9271
- init_vault();
9272
- init_serverLog();
9273
- init_vault_scope();
9274
- import * as fs10 from "fs";
9275
- var EXCLUDED_DIRS3 = /* @__PURE__ */ new Set([
9276
- ".obsidian",
9277
- ".trash",
9278
- ".git",
9279
- "node_modules",
9280
- "templates",
9281
- ".claude",
9282
- ".flywheel"
9283
- ]);
9284
- var MAX_INDEX_FILE_SIZE = 5 * 1024 * 1024;
9285
- var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
9286
- function splitFrontmatter(raw) {
9287
- if (!raw.startsWith("---")) return { frontmatter: "", body: raw };
9288
- const end = raw.indexOf("\n---", 3);
9289
- if (end === -1) return { frontmatter: "", body: raw };
9290
- const yaml = raw.substring(4, end);
9291
- const values = yaml.split("\n").map((line) => line.replace(/^[\s-]*/, "").replace(/^[\w]+:\s*/, "")).filter((v) => v && !v.startsWith("[") && !v.startsWith("{")).join(" ");
9292
- return { frontmatter: values, body: raw.substring(end + 4) };
9293
- }
9294
- var db3 = null;
9295
- function getDb3() {
9296
- return getActiveScopeOrNull()?.stateDb?.db ?? db3;
9297
- }
9298
- var state = {
9299
- ready: false,
9300
- building: false,
9301
- lastBuilt: null,
9302
- noteCount: 0,
9303
- error: null
9304
- };
9305
- function setFTS5Database(database) {
9306
- db3 = database;
9307
- try {
9308
- const row = db3.prepare(
9309
- "SELECT value FROM fts_metadata WHERE key = ?"
9310
- ).get("last_built");
9311
- if (row) {
9312
- const lastBuilt = new Date(row.value);
9313
- const countRow = db3.prepare("SELECT COUNT(*) as count FROM notes_fts").get();
9314
- state = {
9315
- ready: countRow.count > 0,
9316
- building: false,
9317
- lastBuilt,
9318
- noteCount: countRow.count,
9319
- error: null
9320
- };
9321
- }
9322
- } catch {
9323
- }
9324
- }
9325
- function shouldIndexFile2(filePath) {
9326
- const parts = filePath.split("/");
9327
- return !parts.some((part) => EXCLUDED_DIRS3.has(part));
9328
- }
9329
- async function buildFTS5Index(vaultPath2) {
9330
- const db4 = getDb3();
9331
- try {
9332
- state.error = null;
9333
- state.building = true;
9334
- if (!db4) {
9335
- throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
9336
- }
9337
- const files = await scanVault(vaultPath2);
9338
- const indexableFiles = files.filter((f) => shouldIndexFile2(f.path));
9339
- const rows = [];
9340
- for (const file of indexableFiles) {
9341
- try {
9342
- const stats = fs10.statSync(file.absolutePath);
9343
- if (stats.size > MAX_INDEX_FILE_SIZE) {
9467
+ const vaultIndex2 = p.getVaultIndex();
9468
+ const tagDiffs = [];
9469
+ if (p.sd) {
9470
+ const noteTagsForward = /* @__PURE__ */ new Map();
9471
+ for (const [tag, paths] of vaultIndex2.tags) {
9472
+ for (const notePath of paths) {
9473
+ if (!noteTagsForward.has(notePath)) noteTagsForward.set(notePath, /* @__PURE__ */ new Set());
9474
+ noteTagsForward.get(notePath).add(tag);
9475
+ }
9476
+ }
9477
+ for (const event of p.events) {
9478
+ if (event.type === "delete" || !event.path.endsWith(".md")) continue;
9479
+ const currentSet = noteTagsForward.get(event.path) ?? /* @__PURE__ */ new Set();
9480
+ const previousSet = getStoredNoteTags(p.sd, event.path);
9481
+ if (previousSet.size === 0 && currentSet.size > 0) {
9482
+ updateStoredNoteTags(p.sd, event.path, currentSet);
9344
9483
  continue;
9345
9484
  }
9346
- const raw = fs10.readFileSync(file.absolutePath, "utf-8");
9347
- const { frontmatter, body } = splitFrontmatter(raw);
9348
- const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
9349
- rows.push([file.path, title, frontmatter, body]);
9350
- } catch (err) {
9351
- serverLog("fts5", `Skipping ${file.path}: ${err}`, "warn");
9485
+ const added = [...currentSet].filter((t) => !previousSet.has(t));
9486
+ const removed = [...previousSet].filter((t) => !currentSet.has(t));
9487
+ if (added.length > 0 || removed.length > 0) {
9488
+ tagDiffs.push({ file: event.path, added, removed });
9489
+ }
9490
+ updateStoredNoteTags(p.sd, event.path, currentSet);
9352
9491
  }
9353
- }
9354
- const insert = db4.prepare(
9355
- "INSERT INTO notes_fts (path, title, frontmatter, content) VALUES (?, ?, ?, ?)"
9356
- );
9357
- const now = /* @__PURE__ */ new Date();
9358
- const swapAll = db4.transaction(() => {
9359
- db4.exec("DELETE FROM notes_fts");
9360
- for (const row of rows) {
9361
- insert.run(...row);
9492
+ for (const event of p.events) {
9493
+ if (event.type === "delete") {
9494
+ const previousSet = getStoredNoteTags(p.sd, event.path);
9495
+ if (previousSet.size > 0) {
9496
+ tagDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
9497
+ updateStoredNoteTags(p.sd, event.path, /* @__PURE__ */ new Set());
9498
+ }
9499
+ }
9362
9500
  }
9363
- db4.prepare(
9364
- "INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
9365
- ).run("last_built", now.toISOString());
9366
- });
9367
- swapAll();
9368
- const indexed = rows.length;
9369
- state = {
9370
- ready: true,
9371
- building: false,
9372
- lastBuilt: now,
9373
- noteCount: indexed,
9374
- error: null
9375
- };
9376
- serverLog("fts5", `Indexed ${indexed} notes`);
9377
- return state;
9378
- } catch (err) {
9379
- state = {
9380
- ready: false,
9381
- building: false,
9382
- lastBuilt: null,
9383
- noteCount: 0,
9384
- error: err instanceof Error ? err.message : String(err)
9385
- };
9386
- throw err;
9387
- }
9388
- }
9389
- function isIndexStale(_vaultPath) {
9390
- const db4 = getDb3();
9391
- if (!db4) {
9392
- return true;
9393
- }
9394
- try {
9395
- const row = db4.prepare(
9396
- "SELECT value FROM fts_metadata WHERE key = ?"
9397
- ).get("last_built");
9398
- if (!row) {
9399
- return true;
9400
9501
  }
9401
- const lastBuilt = new Date(row.value);
9402
- const age = Date.now() - lastBuilt.getTime();
9403
- return age > STALE_THRESHOLD_MS;
9404
- } catch {
9405
- return true;
9406
- }
9407
- }
9408
- function sanitizeFTS5Query(query) {
9409
- if (!query?.trim()) return "";
9410
- return query.replace(/"/g, '""').replace(/[(){}[\]^~:\-]/g, " ").replace(/\s+/g, " ").trim();
9411
- }
9412
- function searchFTS5(_vaultPath, query, limit = 10) {
9413
- const db4 = getDb3();
9414
- if (!db4) {
9415
- throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
9416
- }
9417
- const sanitized = sanitizeFTS5Query(query);
9418
- if (!sanitized) return [];
9419
- try {
9420
- const stmt = db4.prepare(`
9421
- SELECT
9422
- path,
9423
- title,
9424
- snippet(notes_fts, 3, '<mark>', '</mark>', '...', 64) as snippet
9425
- FROM notes_fts
9426
- WHERE notes_fts MATCH ?
9427
- ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
9428
- LIMIT ?
9429
- `);
9430
- const results = stmt.all(sanitized, limit);
9431
- return results;
9432
- } catch (err) {
9433
- if (err instanceof Error && err.message.includes("fts5:")) {
9434
- return [];
9502
+ const totalTagsAdded = tagDiffs.reduce((s, d) => s + d.added.length, 0);
9503
+ const totalTagsRemoved = tagDiffs.reduce((s, d) => s + d.removed.length, 0);
9504
+ if (tagDiffs.length > 0) {
9505
+ serverLog("watcher", `Tag scan: ${totalTagsAdded} added, ${totalTagsRemoved} removed across ${tagDiffs.length} files`);
9435
9506
  }
9436
- throw err;
9507
+ return { total_added: totalTagsAdded, total_removed: totalTagsRemoved, tag_diffs: tagDiffs };
9437
9508
  }
9438
- }
9439
- function getFTS5State() {
9440
- if (state.building) return { ...state };
9441
- const scopeDb = getDb3();
9442
- if (scopeDb) {
9443
- try {
9444
- const row = scopeDb.prepare(
9445
- "SELECT value FROM fts_metadata WHERE key = ?"
9446
- ).get("last_built");
9447
- const countRow = scopeDb.prepare("SELECT COUNT(*) as count FROM notes_fts").get();
9448
- return {
9449
- ready: countRow.count > 0,
9450
- building: false,
9451
- lastBuilt: row ? new Date(row.value) : null,
9452
- noteCount: countRow.count,
9453
- error: null
9454
- };
9455
- } catch {
9509
+ // ── Step 19: Retrieval co-occurrence ──────────────────────────────
9510
+ async retrievalCooccurrence() {
9511
+ const { p } = this;
9512
+ if (!p.sd) return { skipped: "no sd" };
9513
+ const inserted = mineRetrievalCooccurrence(p.sd);
9514
+ if (inserted > 0) {
9515
+ serverLog("watcher", `Retrieval co-occurrence: ${inserted} new pairs`);
9456
9516
  }
9517
+ return { pairs_inserted: inserted };
9457
9518
  }
9458
- return { ...state };
9459
- }
9460
- function getContentPreview(notePath, maxChars = 300) {
9461
- const db4 = getDb3();
9462
- if (!db4) return null;
9519
+ };
9520
+
9521
+ // src/core/read/logging.ts
9522
+ init_serverLog();
9523
+ import {
9524
+ createLoggerFromConfig,
9525
+ generateSessionId,
9526
+ setSessionId
9527
+ } from "@velvetmonkey/vault-core";
9528
+ var logger = null;
9529
+ async function initializeLogger(vaultPath2) {
9463
9530
  try {
9464
- const row = db4.prepare(
9465
- "SELECT substr(content, 1, ?) as preview FROM notes_fts WHERE path = ?"
9466
- ).get(maxChars + 50, notePath);
9467
- if (!row?.preview) return null;
9468
- const truncated = row.preview.length > maxChars ? row.preview.slice(0, maxChars).replace(/\s\S*$/, "") + "..." : row.preview;
9469
- return truncated;
9470
- } catch {
9471
- return null;
9531
+ const sessionId = generateSessionId();
9532
+ setSessionId(sessionId);
9533
+ logger = await createLoggerFromConfig(vaultPath2, "flywheel");
9534
+ } catch (error) {
9535
+ serverLog("server", `Failed to initialize logger: ${error}`, "error");
9536
+ logger = null;
9472
9537
  }
9473
9538
  }
9474
- function countFTS5Mentions(term) {
9475
- const db4 = getDb3();
9476
- if (!db4) return 0;
9539
+ function getLogger() {
9540
+ return logger;
9541
+ }
9542
+
9543
+ // src/index.ts
9544
+ init_wikilinks();
9545
+
9546
+ // src/core/write/logging.ts
9547
+ init_serverLog();
9548
+ import {
9549
+ createLoggerFromConfig as createLoggerFromConfig2,
9550
+ generateSessionId as generateSessionId2,
9551
+ setSessionId as setSessionId2
9552
+ } from "@velvetmonkey/vault-core";
9553
+ var logger2 = null;
9554
+ async function initializeLogger2(vaultPath2) {
9477
9555
  try {
9478
- const result = db4.prepare(
9479
- "SELECT COUNT(*) as cnt FROM notes_fts WHERE content MATCH ?"
9480
- ).get(`"${term}"`);
9481
- return result?.cnt ?? 0;
9482
- } catch {
9483
- return 0;
9556
+ const sessionId = generateSessionId2();
9557
+ setSessionId2(sessionId);
9558
+ logger2 = await createLoggerFromConfig2(vaultPath2, "write");
9559
+ } catch (error) {
9560
+ serverLog("server", `Failed to initialize logger: ${error}`, "error");
9561
+ logger2 = null;
9484
9562
  }
9485
9563
  }
9564
+ async function flushLogs() {
9565
+ if (logger2) await logger2.flush();
9566
+ }
9486
9567
 
9487
9568
  // src/index.ts
9488
9569
  init_embeddings();
@@ -10183,8 +10264,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
10183
10264
  }
10184
10265
  }
10185
10266
  }
10186
- return Array.from(noteMap.entries()).map(([path36, stats]) => ({
10187
- path: path36,
10267
+ return Array.from(noteMap.entries()).map(([path37, stats]) => ({
10268
+ path: path37,
10188
10269
  access_count: stats.access_count,
10189
10270
  last_accessed: stats.last_accessed,
10190
10271
  tools_used: Array.from(stats.tools)
@@ -10715,7 +10796,7 @@ var TOOL_CATEGORY = {
10715
10796
  predict_stale_notes: "temporal",
10716
10797
  track_concept_evolution: "temporal",
10717
10798
  temporal_summary: "temporal",
10718
- // diagnostics (14 tools) -- vault health, stats, config, activity, merges
10799
+ // diagnostics (18 tools) -- vault health, stats, config, activity, merges, doctor, trust, benchmark, history
10719
10800
  health_check: "diagnostics",
10720
10801
  get_vault_stats: "diagnostics",
10721
10802
  get_folder_structure: "diagnostics",
@@ -10856,9 +10937,9 @@ Use "note_intelligence" for per-note analysis (completeness, quality, suggestion
10856
10937
  }
10857
10938
 
10858
10939
  // src/tool-registry.ts
10859
- import * as path34 from "path";
10860
- import { dirname as dirname5, join as join18 } from "path";
10861
- import { statSync as statSync5, readFileSync as readFileSync5 } from "fs";
10940
+ import * as path35 from "path";
10941
+ import { dirname as dirname5, join as join19 } from "path";
10942
+ import { statSync as statSync6, readFileSync as readFileSync5 } from "fs";
10862
10943
  import { fileURLToPath } from "url";
10863
10944
  import { z as z38 } from "zod";
10864
10945
  import { getSessionId } from "@velvetmonkey/vault-core";
@@ -10866,7 +10947,7 @@ init_vault_scope();
10866
10947
 
10867
10948
  // src/tools/read/graph.ts
10868
10949
  import * as fs11 from "fs";
10869
- import * as path15 from "path";
10950
+ import * as path16 from "path";
10870
10951
  import { z } from "zod";
10871
10952
 
10872
10953
  // src/core/read/constants.ts
@@ -11317,7 +11398,7 @@ function requireIndex() {
11317
11398
  // src/tools/read/graph.ts
11318
11399
  async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
11319
11400
  try {
11320
- const fullPath = path15.join(vaultPath2, sourcePath);
11401
+ const fullPath = path16.join(vaultPath2, sourcePath);
11321
11402
  const content = await fs11.promises.readFile(fullPath, "utf-8");
11322
11403
  const allLines = content.split("\n");
11323
11404
  let fmLines = 0;
@@ -12108,14 +12189,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath, getStateDb3 = ()
12108
12189
  };
12109
12190
  function findSimilarEntity2(target, entities) {
12110
12191
  const targetLower = target.toLowerCase();
12111
- for (const [name, path36] of entities) {
12192
+ for (const [name, path37] of entities) {
12112
12193
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
12113
- return path36;
12194
+ return path37;
12114
12195
  }
12115
12196
  }
12116
- for (const [name, path36] of entities) {
12197
+ for (const [name, path37] of entities) {
12117
12198
  if (name.includes(targetLower) || targetLower.includes(name)) {
12118
- return path36;
12199
+ return path37;
12119
12200
  }
12120
12201
  }
12121
12202
  return void 0;
@@ -12953,8 +13034,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
12953
13034
  daily_counts: z4.record(z4.number())
12954
13035
  }).describe("Activity summary for the last 7 days")
12955
13036
  };
12956
- function isPeriodicNote3(path36) {
12957
- const filename = path36.split("/").pop() || "";
13037
+ function isPeriodicNote3(path37) {
13038
+ const filename = path37.split("/").pop() || "";
12958
13039
  const nameWithoutExt = filename.replace(/\.md$/, "");
12959
13040
  const patterns = [
12960
13041
  /^\d{4}-\d{2}-\d{2}$/,
@@ -12969,7 +13050,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
12969
13050
  // YYYY (yearly)
12970
13051
  ];
12971
13052
  const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
12972
- const folder = path36.split("/")[0]?.toLowerCase() || "";
13053
+ const folder = path37.split("/")[0]?.toLowerCase() || "";
12973
13054
  return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
12974
13055
  }
12975
13056
  server2.registerTool(
@@ -13818,13 +13899,13 @@ function multiHopBackfill(primaryResults, index, stateDb2, config = {}) {
13818
13899
  candidates.sort((a, b) => b.score - a.score);
13819
13900
  return candidates.slice(0, cfg.maxBackfill).map((c) => c.result);
13820
13901
  }
13821
- function scoreCandidate(path36, index, stateDb2) {
13822
- const note = index.notes.get(path36);
13902
+ function scoreCandidate(path37, index, stateDb2) {
13903
+ const note = index.notes.get(path37);
13823
13904
  const decay = recencyDecay(note?.modified);
13824
13905
  let hubScore = 1;
13825
13906
  if (stateDb2) {
13826
13907
  try {
13827
- const title = note?.title ?? path36.replace(/\.md$/, "").split("/").pop() ?? "";
13908
+ const title = note?.title ?? path37.replace(/\.md$/, "").split("/").pop() ?? "";
13828
13909
  const entity = getEntityByName3(stateDb2, title);
13829
13910
  if (entity) hubScore = entity.hubScore ?? 1;
13830
13911
  } catch {
@@ -14352,7 +14433,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
14352
14433
 
14353
14434
  // src/tools/read/system.ts
14354
14435
  import * as fs14 from "fs";
14355
- import * as path16 from "path";
14436
+ import * as path17 from "path";
14356
14437
  import { z as z6 } from "zod";
14357
14438
  import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
14358
14439
 
@@ -14665,7 +14746,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
14665
14746
  continue;
14666
14747
  }
14667
14748
  try {
14668
- const fullPath = path16.join(vaultPath2, note.path);
14749
+ const fullPath = path17.join(vaultPath2, note.path);
14669
14750
  const content = await fs14.promises.readFile(fullPath, "utf-8");
14670
14751
  const lines = content.split("\n");
14671
14752
  for (let i = 0; i < lines.length; i++) {
@@ -14926,7 +15007,7 @@ import { z as z7 } from "zod";
14926
15007
 
14927
15008
  // src/tools/read/structure.ts
14928
15009
  import * as fs15 from "fs";
14929
- import * as path17 from "path";
15010
+ import * as path18 from "path";
14930
15011
  var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
14931
15012
  function extractHeadings2(content) {
14932
15013
  const lines = content.split("\n");
@@ -14980,7 +15061,7 @@ function buildSections(headings, totalLines) {
14980
15061
  async function getNoteStructure(index, notePath, vaultPath2) {
14981
15062
  const note = index.notes.get(notePath);
14982
15063
  if (!note) return null;
14983
- const absolutePath = path17.join(vaultPath2, notePath);
15064
+ const absolutePath = path18.join(vaultPath2, notePath);
14984
15065
  let content;
14985
15066
  try {
14986
15067
  content = await fs15.promises.readFile(absolutePath, "utf-8");
@@ -15003,7 +15084,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
15003
15084
  async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
15004
15085
  const note = index.notes.get(notePath);
15005
15086
  if (!note) return null;
15006
- const absolutePath = path17.join(vaultPath2, notePath);
15087
+ const absolutePath = path18.join(vaultPath2, notePath);
15007
15088
  let content;
15008
15089
  try {
15009
15090
  content = await fs15.promises.readFile(absolutePath, "utf-8");
@@ -15045,7 +15126,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
15045
15126
  const results = [];
15046
15127
  for (const note of index.notes.values()) {
15047
15128
  if (folder && !note.path.startsWith(folder)) continue;
15048
- const absolutePath = path17.join(vaultPath2, note.path);
15129
+ const absolutePath = path18.join(vaultPath2, note.path);
15049
15130
  let content;
15050
15131
  try {
15051
15132
  content = await fs15.promises.readFile(absolutePath, "utf-8");
@@ -15080,30 +15161,30 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
15080
15161
  include_content: z7.boolean().default(true).describe("Include the text content under each top-level section. Set false to get structure only.")
15081
15162
  }
15082
15163
  },
15083
- async ({ path: path36, include_content }) => {
15164
+ async ({ path: path37, include_content }) => {
15084
15165
  const index = getIndex();
15085
15166
  const vaultPath2 = getVaultPath();
15086
- const result = await getNoteStructure(index, path36, vaultPath2);
15167
+ const result = await getNoteStructure(index, path37, vaultPath2);
15087
15168
  if (!result) {
15088
15169
  return {
15089
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path36 }, null, 2) }]
15170
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path37 }, null, 2) }]
15090
15171
  };
15091
15172
  }
15092
15173
  if (include_content) {
15093
15174
  for (const section of result.sections) {
15094
- const sectionResult = await getSectionContent(index, path36, section.heading.text, vaultPath2, true);
15175
+ const sectionResult = await getSectionContent(index, path37, section.heading.text, vaultPath2, true);
15095
15176
  if (sectionResult) {
15096
15177
  section.content = sectionResult.content;
15097
15178
  }
15098
15179
  }
15099
15180
  }
15100
- const note = index.notes.get(path36);
15181
+ const note = index.notes.get(path37);
15101
15182
  const enriched = { ...result };
15102
15183
  if (note) {
15103
15184
  enriched.frontmatter = note.frontmatter;
15104
15185
  enriched.tags = note.tags;
15105
15186
  enriched.aliases = note.aliases;
15106
- const normalizedPath = path36.toLowerCase().replace(/\.md$/, "");
15187
+ const normalizedPath = path37.toLowerCase().replace(/\.md$/, "");
15107
15188
  const backlinks = index.backlinks.get(normalizedPath) || [];
15108
15189
  enriched.backlink_count = backlinks.length;
15109
15190
  enriched.outlink_count = note.outlinks.length;
@@ -15136,15 +15217,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
15136
15217
  include_subheadings: z7.boolean().default(true).describe("Include content under subheadings")
15137
15218
  }
15138
15219
  },
15139
- async ({ path: path36, heading, include_subheadings }) => {
15220
+ async ({ path: path37, heading, include_subheadings }) => {
15140
15221
  const index = getIndex();
15141
15222
  const vaultPath2 = getVaultPath();
15142
- const result = await getSectionContent(index, path36, heading, vaultPath2, include_subheadings);
15223
+ const result = await getSectionContent(index, path37, heading, vaultPath2, include_subheadings);
15143
15224
  if (!result) {
15144
15225
  return {
15145
15226
  content: [{ type: "text", text: JSON.stringify({
15146
15227
  error: "Section not found",
15147
- path: path36,
15228
+ path: path37,
15148
15229
  heading
15149
15230
  }, null, 2) }]
15150
15231
  };
@@ -15198,16 +15279,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
15198
15279
  offset: z7.coerce.number().default(0).describe("Number of results to skip (for pagination)")
15199
15280
  }
15200
15281
  },
15201
- async ({ path: path36, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
15282
+ async ({ path: path37, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
15202
15283
  const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
15203
15284
  const index = getIndex();
15204
15285
  const vaultPath2 = getVaultPath();
15205
15286
  const config = getConfig2();
15206
- if (path36) {
15207
- const result2 = await getTasksFromNote(index, path36, vaultPath2, config.exclude_task_tags || []);
15287
+ if (path37) {
15288
+ const result2 = await getTasksFromNote(index, path37, vaultPath2, config.exclude_task_tags || []);
15208
15289
  if (!result2) {
15209
15290
  return {
15210
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path36 }, null, 2) }]
15291
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path37 }, null, 2) }]
15211
15292
  };
15212
15293
  }
15213
15294
  let filtered = result2;
@@ -15217,7 +15298,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
15217
15298
  const paged2 = filtered.slice(offset, offset + limit);
15218
15299
  return {
15219
15300
  content: [{ type: "text", text: JSON.stringify({
15220
- path: path36,
15301
+ path: path37,
15221
15302
  total_count: filtered.length,
15222
15303
  returned_count: paged2.length,
15223
15304
  open: result2.filter((t) => t.status === "open").length,
@@ -15373,7 +15454,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
15373
15454
  // src/tools/read/migrations.ts
15374
15455
  import { z as z8 } from "zod";
15375
15456
  import * as fs16 from "fs/promises";
15376
- import * as path18 from "path";
15457
+ import * as path19 from "path";
15377
15458
  import matter2 from "gray-matter";
15378
15459
  function getNotesInFolder(index, folder) {
15379
15460
  const notes = [];
@@ -15386,7 +15467,7 @@ function getNotesInFolder(index, folder) {
15386
15467
  return notes;
15387
15468
  }
15388
15469
  async function readFileContent(notePath, vaultPath2) {
15389
- const fullPath = path18.join(vaultPath2, notePath);
15470
+ const fullPath = path19.join(vaultPath2, notePath);
15390
15471
  try {
15391
15472
  return await fs16.readFile(fullPath, "utf-8");
15392
15473
  } catch {
@@ -15394,7 +15475,7 @@ async function readFileContent(notePath, vaultPath2) {
15394
15475
  }
15395
15476
  }
15396
15477
  async function writeFileContent(notePath, vaultPath2, content) {
15397
- const fullPath = path18.join(vaultPath2, notePath);
15478
+ const fullPath = path19.join(vaultPath2, notePath);
15398
15479
  try {
15399
15480
  await fs16.writeFile(fullPath, content, "utf-8");
15400
15481
  return true;
@@ -15575,7 +15656,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
15575
15656
 
15576
15657
  // src/tools/read/graphAnalysis.ts
15577
15658
  import fs17 from "node:fs";
15578
- import path19 from "node:path";
15659
+ import path20 from "node:path";
15579
15660
  import { z as z9 } from "zod";
15580
15661
 
15581
15662
  // src/tools/read/schema.ts
@@ -16101,7 +16182,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb3
16101
16182
  const scored = allNotes.map((note) => {
16102
16183
  let wordCount = 0;
16103
16184
  try {
16104
- const content = fs17.readFileSync(path19.join(vaultPath2, note.path), "utf-8");
16185
+ const content = fs17.readFileSync(path20.join(vaultPath2, note.path), "utf-8");
16105
16186
  const body = content.replace(/^---[\s\S]*?---\n?/, "");
16106
16187
  wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
16107
16188
  } catch {
@@ -16732,12 +16813,12 @@ import { z as z12 } from "zod";
16732
16813
 
16733
16814
  // src/tools/read/bidirectional.ts
16734
16815
  import * as fs18 from "fs/promises";
16735
- import * as path20 from "path";
16816
+ import * as path21 from "path";
16736
16817
  import matter3 from "gray-matter";
16737
16818
  var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
16738
16819
  var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
16739
16820
  async function readFileContent2(notePath, vaultPath2) {
16740
- const fullPath = path20.join(vaultPath2, notePath);
16821
+ const fullPath = path21.join(vaultPath2, notePath);
16741
16822
  try {
16742
16823
  return await fs18.readFile(fullPath, "utf-8");
16743
16824
  } catch {
@@ -16916,10 +16997,10 @@ async function suggestWikilinksInFrontmatter(index, notePath, vaultPath2) {
16916
16997
 
16917
16998
  // src/tools/read/computed.ts
16918
16999
  import * as fs19 from "fs/promises";
16919
- import * as path21 from "path";
17000
+ import * as path22 from "path";
16920
17001
  import matter4 from "gray-matter";
16921
17002
  async function readFileContent3(notePath, vaultPath2) {
16922
- const fullPath = path21.join(vaultPath2, notePath);
17003
+ const fullPath = path22.join(vaultPath2, notePath);
16923
17004
  try {
16924
17005
  return await fs19.readFile(fullPath, "utf-8");
16925
17006
  } catch {
@@ -16927,7 +17008,7 @@ async function readFileContent3(notePath, vaultPath2) {
16927
17008
  }
16928
17009
  }
16929
17010
  async function getFileStats(notePath, vaultPath2) {
16930
- const fullPath = path21.join(vaultPath2, notePath);
17011
+ const fullPath = path22.join(vaultPath2, notePath);
16931
17012
  try {
16932
17013
  const stats = await fs19.stat(fullPath);
16933
17014
  return {
@@ -17199,7 +17280,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath, getConfi
17199
17280
  init_writer();
17200
17281
  import { z as z13 } from "zod";
17201
17282
  import fs23 from "fs/promises";
17202
- import path24 from "path";
17283
+ import path25 from "path";
17203
17284
 
17204
17285
  // src/core/write/validator.ts
17205
17286
  var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
@@ -17418,7 +17499,7 @@ init_writer();
17418
17499
  init_wikilinks();
17419
17500
  init_wikilinkFeedback();
17420
17501
  import fs22 from "fs/promises";
17421
- import path23 from "path";
17502
+ import path24 from "path";
17422
17503
  function formatMcpResult(result) {
17423
17504
  if (result.tokensEstimate === void 0 || result.tokensEstimate === 0) {
17424
17505
  result.tokensEstimate = estimateTokens(result);
@@ -17466,7 +17547,7 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
17466
17547
  }
17467
17548
  async function getPolicyHint(vaultPath2) {
17468
17549
  try {
17469
- const policiesDir = path23.join(vaultPath2, ".claude", "policies");
17550
+ const policiesDir = path24.join(vaultPath2, ".claude", "policies");
17470
17551
  const files = await fs22.readdir(policiesDir);
17471
17552
  const yamlFiles = files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
17472
17553
  if (yamlFiles.length > 0) {
@@ -17478,7 +17559,7 @@ async function getPolicyHint(vaultPath2) {
17478
17559
  return "";
17479
17560
  }
17480
17561
  async function ensureFileExists(vaultPath2, notePath) {
17481
- const fullPath = path23.join(vaultPath2, notePath);
17562
+ const fullPath = path24.join(vaultPath2, notePath);
17482
17563
  try {
17483
17564
  await fs22.access(fullPath);
17484
17565
  return null;
@@ -17656,7 +17737,7 @@ async function executeCreateNote(options) {
17656
17737
  if (!pathCheck.valid) {
17657
17738
  return { success: false, result: errorResult(notePath, `Path blocked: ${pathCheck.reason}`), filesWritten: [] };
17658
17739
  }
17659
- const fullPath = path23.join(vaultPath2, notePath);
17740
+ const fullPath = path24.join(vaultPath2, notePath);
17660
17741
  let fileExists = false;
17661
17742
  try {
17662
17743
  await fs22.access(fullPath);
@@ -17666,7 +17747,7 @@ async function executeCreateNote(options) {
17666
17747
  if (fileExists && !overwrite) {
17667
17748
  return { success: false, result: errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`), filesWritten: [] };
17668
17749
  }
17669
- await fs22.mkdir(path23.dirname(fullPath), { recursive: true });
17750
+ await fs22.mkdir(path24.dirname(fullPath), { recursive: true });
17670
17751
  const { maybeApplyWikilinks: maybeApplyWikilinks2 } = await Promise.resolve().then(() => (init_wikilinks(), wikilinks_exports));
17671
17752
  const { content: processedContent } = maybeApplyWikilinks2(content, skipWikilinks ?? false, notePath);
17672
17753
  let finalFrontmatter = frontmatter;
@@ -17700,7 +17781,7 @@ async function executeDeleteNote(options) {
17700
17781
  if (!pathCheck.valid) {
17701
17782
  return { success: false, result: errorResult(notePath, `Path blocked: ${pathCheck.reason}`), filesWritten: [] };
17702
17783
  }
17703
- const fullPath = path23.join(vaultPath2, notePath);
17784
+ const fullPath = path24.join(vaultPath2, notePath);
17704
17785
  try {
17705
17786
  await fs22.access(fullPath);
17706
17787
  } catch {
@@ -17724,10 +17805,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
17724
17805
  if (!validation.valid) {
17725
17806
  throw new Error(`Path blocked: ${validation.reason}`);
17726
17807
  }
17727
- const fullPath = path24.join(vaultPath2, notePath);
17728
- await fs23.mkdir(path24.dirname(fullPath), { recursive: true });
17808
+ const fullPath = path25.join(vaultPath2, notePath);
17809
+ await fs23.mkdir(path25.dirname(fullPath), { recursive: true });
17729
17810
  const templates = config.templates || {};
17730
- const filename = path24.basename(notePath, ".md").toLowerCase();
17811
+ const filename = path25.basename(notePath, ".md").toLowerCase();
17731
17812
  let templatePath;
17732
17813
  const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
17733
17814
  const weeklyPattern = /^\d{4}-W\d{2}/;
@@ -17748,10 +17829,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
17748
17829
  let templateContent;
17749
17830
  if (templatePath) {
17750
17831
  try {
17751
- const absTemplatePath = path24.join(vaultPath2, templatePath);
17832
+ const absTemplatePath = path25.join(vaultPath2, templatePath);
17752
17833
  templateContent = await fs23.readFile(absTemplatePath, "utf-8");
17753
17834
  } catch {
17754
- const title = path24.basename(notePath, ".md");
17835
+ const title = path25.basename(notePath, ".md");
17755
17836
  templateContent = `---
17756
17837
  ---
17757
17838
 
@@ -17760,7 +17841,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
17760
17841
  templatePath = void 0;
17761
17842
  }
17762
17843
  } else {
17763
- const title = path24.basename(notePath, ".md");
17844
+ const title = path25.basename(notePath, ".md");
17764
17845
  templateContent = `---
17765
17846
  ---
17766
17847
 
@@ -17769,7 +17850,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
17769
17850
  }
17770
17851
  const now = /* @__PURE__ */ new Date();
17771
17852
  const dateStr = now.toISOString().split("T")[0];
17772
- templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path24.basename(notePath, ".md"));
17853
+ templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path25.basename(notePath, ".md"));
17773
17854
  const matter9 = (await import("gray-matter")).default;
17774
17855
  const parsed = matter9(templateContent);
17775
17856
  if (!parsed.data.date) {
@@ -17810,7 +17891,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
17810
17891
  let noteCreated = false;
17811
17892
  let templateUsed;
17812
17893
  if (create_if_missing && !dry_run) {
17813
- const fullPath = path24.join(vaultPath2, notePath);
17894
+ const fullPath = path25.join(vaultPath2, notePath);
17814
17895
  try {
17815
17896
  await fs23.access(fullPath);
17816
17897
  } catch {
@@ -17821,7 +17902,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
17821
17902
  }
17822
17903
  }
17823
17904
  if (create_if_missing && dry_run) {
17824
- const fullPath = path24.join(vaultPath2, notePath);
17905
+ const fullPath = path25.join(vaultPath2, notePath);
17825
17906
  try {
17826
17907
  await fs23.access(fullPath);
17827
17908
  } catch {
@@ -18309,7 +18390,7 @@ init_writer();
18309
18390
  init_wikilinks();
18310
18391
  import { z as z16 } from "zod";
18311
18392
  import fs24 from "fs/promises";
18312
- import path25 from "path";
18393
+ import path26 from "path";
18313
18394
  function registerNoteTools(server2, getVaultPath, getIndex) {
18314
18395
  server2.tool(
18315
18396
  "vault_create_note",
@@ -18335,23 +18416,23 @@ function registerNoteTools(server2, getVaultPath, getIndex) {
18335
18416
  if (!validatePath(vaultPath2, notePath)) {
18336
18417
  return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
18337
18418
  }
18338
- const fullPath = path25.join(vaultPath2, notePath);
18419
+ const fullPath = path26.join(vaultPath2, notePath);
18339
18420
  const existsCheck = await ensureFileExists(vaultPath2, notePath);
18340
18421
  if (existsCheck === null && !overwrite) {
18341
18422
  return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
18342
18423
  }
18343
- const dir = path25.dirname(fullPath);
18424
+ const dir = path26.dirname(fullPath);
18344
18425
  await fs24.mkdir(dir, { recursive: true });
18345
18426
  let effectiveContent = content;
18346
18427
  let effectiveFrontmatter = frontmatter;
18347
18428
  if (template) {
18348
- const templatePath = path25.join(vaultPath2, template);
18429
+ const templatePath = path26.join(vaultPath2, template);
18349
18430
  try {
18350
18431
  const raw = await fs24.readFile(templatePath, "utf-8");
18351
18432
  const matter9 = (await import("gray-matter")).default;
18352
18433
  const parsed = matter9(raw);
18353
18434
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
18354
- const title = path25.basename(notePath, ".md");
18435
+ const title = path26.basename(notePath, ".md");
18355
18436
  let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
18356
18437
  if (content) {
18357
18438
  templateContent = templateContent.trimEnd() + "\n\n" + content;
@@ -18370,7 +18451,7 @@ function registerNoteTools(server2, getVaultPath, getIndex) {
18370
18451
  effectiveFrontmatter.created = now.toISOString();
18371
18452
  }
18372
18453
  const warnings = [];
18373
- const noteName = path25.basename(notePath, ".md");
18454
+ const noteName = path26.basename(notePath, ".md");
18374
18455
  const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
18375
18456
  const preflight = await checkPreflightSimilarity(noteName);
18376
18457
  if (preflight.existingEntity) {
@@ -18511,7 +18592,7 @@ ${sources}`;
18511
18592
  }
18512
18593
  return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
18513
18594
  }
18514
- const fullPath = path25.join(vaultPath2, notePath);
18595
+ const fullPath = path26.join(vaultPath2, notePath);
18515
18596
  await fs24.unlink(fullPath);
18516
18597
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
18517
18598
  const message = backlinkWarning ? `Deleted note: ${notePath}
@@ -18533,7 +18614,7 @@ init_git();
18533
18614
  init_wikilinks();
18534
18615
  import { z as z17 } from "zod";
18535
18616
  import fs25 from "fs/promises";
18536
- import path26 from "path";
18617
+ import path27 from "path";
18537
18618
  import matter6 from "gray-matter";
18538
18619
  function escapeRegex(str) {
18539
18620
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -18552,7 +18633,7 @@ function extractWikilinks2(content) {
18552
18633
  return wikilinks;
18553
18634
  }
18554
18635
  function getTitleFromPath(filePath) {
18555
- return path26.basename(filePath, ".md");
18636
+ return path27.basename(filePath, ".md");
18556
18637
  }
18557
18638
  async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
18558
18639
  const results = [];
@@ -18561,7 +18642,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
18561
18642
  const files = [];
18562
18643
  const entries = await fs25.readdir(dir, { withFileTypes: true });
18563
18644
  for (const entry of entries) {
18564
- const fullPath = path26.join(dir, entry.name);
18645
+ const fullPath = path27.join(dir, entry.name);
18565
18646
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
18566
18647
  files.push(...await scanDir(fullPath));
18567
18648
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -18572,7 +18653,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
18572
18653
  }
18573
18654
  const allFiles = await scanDir(vaultPath2);
18574
18655
  for (const filePath of allFiles) {
18575
- const relativePath = path26.relative(vaultPath2, filePath);
18656
+ const relativePath = path27.relative(vaultPath2, filePath);
18576
18657
  const content = await fs25.readFile(filePath, "utf-8");
18577
18658
  const wikilinks = extractWikilinks2(content);
18578
18659
  const matchingLinks = [];
@@ -18659,8 +18740,8 @@ function registerMoveNoteTools(server2, getVaultPath) {
18659
18740
  };
18660
18741
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
18661
18742
  }
18662
- const oldFullPath = path26.join(vaultPath2, oldPath);
18663
- const newFullPath = path26.join(vaultPath2, newPath);
18743
+ const oldFullPath = path27.join(vaultPath2, oldPath);
18744
+ const newFullPath = path27.join(vaultPath2, newPath);
18664
18745
  try {
18665
18746
  await fs25.access(oldFullPath);
18666
18747
  } catch {
@@ -18741,7 +18822,7 @@ function registerMoveNoteTools(server2, getVaultPath) {
18741
18822
  };
18742
18823
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
18743
18824
  }
18744
- const destDir = path26.dirname(newFullPath);
18825
+ const destDir = path27.dirname(newFullPath);
18745
18826
  await fs25.mkdir(destDir, { recursive: true });
18746
18827
  await fs25.rename(oldFullPath, newFullPath);
18747
18828
  let gitCommit;
@@ -18817,10 +18898,10 @@ function registerMoveNoteTools(server2, getVaultPath) {
18817
18898
  if (sanitizedTitle !== newTitle) {
18818
18899
  console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
18819
18900
  }
18820
- const fullPath = path26.join(vaultPath2, notePath);
18821
- const dir = path26.dirname(notePath);
18822
- const newPath = dir === "." ? `${sanitizedTitle}.md` : path26.join(dir, `${sanitizedTitle}.md`);
18823
- const newFullPath = path26.join(vaultPath2, newPath);
18901
+ const fullPath = path27.join(vaultPath2, notePath);
18902
+ const dir = path27.dirname(notePath);
18903
+ const newPath = dir === "." ? `${sanitizedTitle}.md` : path27.join(dir, `${sanitizedTitle}.md`);
18904
+ const newFullPath = path27.join(vaultPath2, newPath);
18824
18905
  try {
18825
18906
  await fs25.access(fullPath);
18826
18907
  } catch {
@@ -19351,7 +19432,7 @@ init_schema();
19351
19432
  // src/core/write/policy/parser.ts
19352
19433
  init_schema();
19353
19434
  import fs27 from "fs/promises";
19354
- import path27 from "path";
19435
+ import path28 from "path";
19355
19436
  import matter7 from "gray-matter";
19356
19437
  function parseYaml(content) {
19357
19438
  const parsed = matter7(`---
@@ -19400,13 +19481,13 @@ async function loadPolicyFile(filePath) {
19400
19481
  }
19401
19482
  }
19402
19483
  async function loadPolicy(vaultPath2, policyName) {
19403
- const policiesDir = path27.join(vaultPath2, ".claude", "policies");
19404
- const policyPath = path27.join(policiesDir, `${policyName}.yaml`);
19484
+ const policiesDir = path28.join(vaultPath2, ".claude", "policies");
19485
+ const policyPath = path28.join(policiesDir, `${policyName}.yaml`);
19405
19486
  try {
19406
19487
  await fs27.access(policyPath);
19407
19488
  return loadPolicyFile(policyPath);
19408
19489
  } catch {
19409
- const ymlPath = path27.join(policiesDir, `${policyName}.yml`);
19490
+ const ymlPath = path28.join(policiesDir, `${policyName}.yml`);
19410
19491
  try {
19411
19492
  await fs27.access(ymlPath);
19412
19493
  return loadPolicyFile(ymlPath);
@@ -19549,7 +19630,7 @@ init_writer();
19549
19630
  init_git();
19550
19631
  init_wikilinks();
19551
19632
  import fs29 from "fs/promises";
19552
- import path29 from "path";
19633
+ import path30 from "path";
19553
19634
  init_constants();
19554
19635
  async function executeStep(step, vaultPath2, context, conditionResults, searchFn) {
19555
19636
  const { execute, reason } = shouldStepExecute(step.when, conditionResults);
@@ -19757,7 +19838,7 @@ async function executeToggleTask(params, vaultPath2) {
19757
19838
  const notePath = String(params.path || "");
19758
19839
  const task = String(params.task || "");
19759
19840
  const section = params.section ? String(params.section) : void 0;
19760
- const fullPath = path29.join(vaultPath2, notePath);
19841
+ const fullPath = path30.join(vaultPath2, notePath);
19761
19842
  try {
19762
19843
  await fs29.access(fullPath);
19763
19844
  } catch {
@@ -20040,7 +20121,7 @@ async function rollbackChanges(vaultPath2, originalContents, filesModified) {
20040
20121
  const pathCheck = await validatePathSecure(vaultPath2, filePath);
20041
20122
  if (!pathCheck.valid) continue;
20042
20123
  const original = originalContents.get(filePath);
20043
- const fullPath = path29.join(vaultPath2, filePath);
20124
+ const fullPath = path30.join(vaultPath2, filePath);
20044
20125
  if (original === null) {
20045
20126
  try {
20046
20127
  await fs29.unlink(fullPath);
@@ -20095,9 +20176,9 @@ async function previewPolicy(policy, vaultPath2, variables) {
20095
20176
 
20096
20177
  // src/core/write/policy/storage.ts
20097
20178
  import fs30 from "fs/promises";
20098
- import path30 from "path";
20179
+ import path31 from "path";
20099
20180
  function getPoliciesDir(vaultPath2) {
20100
- return path30.join(vaultPath2, ".claude", "policies");
20181
+ return path31.join(vaultPath2, ".claude", "policies");
20101
20182
  }
20102
20183
  async function ensurePoliciesDir(vaultPath2) {
20103
20184
  const dir = getPoliciesDir(vaultPath2);
@@ -20112,7 +20193,7 @@ async function listPolicies(vaultPath2) {
20112
20193
  if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
20113
20194
  continue;
20114
20195
  }
20115
- const filePath = path30.join(dir, file);
20196
+ const filePath = path31.join(dir, file);
20116
20197
  const stat5 = await fs30.stat(filePath);
20117
20198
  const content = await fs30.readFile(filePath, "utf-8");
20118
20199
  const metadata = extractPolicyMetadata(content);
@@ -20137,7 +20218,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
20137
20218
  const dir = getPoliciesDir(vaultPath2);
20138
20219
  await ensurePoliciesDir(vaultPath2);
20139
20220
  const filename = `${policyName}.yaml`;
20140
- const filePath = path30.join(dir, filename);
20221
+ const filePath = path31.join(dir, filename);
20141
20222
  if (!overwrite) {
20142
20223
  try {
20143
20224
  await fs30.access(filePath);
@@ -20683,7 +20764,7 @@ import { z as z22 } from "zod";
20683
20764
 
20684
20765
  // src/core/write/tagRename.ts
20685
20766
  import * as fs31 from "fs/promises";
20686
- import * as path31 from "path";
20767
+ import * as path32 from "path";
20687
20768
  import matter8 from "gray-matter";
20688
20769
  import { getProtectedZones as getProtectedZones2 } from "@velvetmonkey/vault-core";
20689
20770
  function getNotesInFolder3(index, folder) {
@@ -20789,7 +20870,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
20789
20870
  const previews = [];
20790
20871
  let totalChanges = 0;
20791
20872
  for (const note of affectedNotes) {
20792
- const fullPath = path31.join(vaultPath2, note.path);
20873
+ const fullPath = path32.join(vaultPath2, note.path);
20793
20874
  let fileContent;
20794
20875
  try {
20795
20876
  fileContent = await fs31.readFile(fullPath, "utf-8");
@@ -22097,7 +22178,7 @@ init_wikilinks();
22097
22178
  init_wikilinkFeedback();
22098
22179
  import { z as z29 } from "zod";
22099
22180
  import * as fs32 from "fs/promises";
22100
- import * as path32 from "path";
22181
+ import * as path33 from "path";
22101
22182
  import { scanVaultEntities as scanVaultEntities3, SCHEMA_VERSION as SCHEMA_VERSION2 } from "@velvetmonkey/vault-core";
22102
22183
  init_embeddings();
22103
22184
  function hasSkipWikilinks(content) {
@@ -22113,13 +22194,13 @@ async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
22113
22194
  const entries = await fs32.readdir(dirPath, { withFileTypes: true });
22114
22195
  for (const entry of entries) {
22115
22196
  if (entry.name.startsWith(".")) continue;
22116
- const fullPath = path32.join(dirPath, entry.name);
22197
+ const fullPath = path33.join(dirPath, entry.name);
22117
22198
  if (entry.isDirectory()) {
22118
22199
  if (excludeFolders.some((f) => entry.name.toLowerCase() === f.toLowerCase())) continue;
22119
22200
  const sub = await collectMarkdownFiles(fullPath, basePath, excludeFolders);
22120
22201
  results.push(...sub);
22121
22202
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
22122
- results.push(path32.relative(basePath, fullPath));
22203
+ results.push(path33.relative(basePath, fullPath));
22123
22204
  }
22124
22205
  }
22125
22206
  } catch {
@@ -22149,7 +22230,7 @@ var EXCLUDE_FOLDERS = [
22149
22230
  ];
22150
22231
  function buildStatusReport(stateDb2, vaultPath2) {
22151
22232
  const recommendations = [];
22152
- const dbPath = path32.join(vaultPath2, ".flywheel", "state.db");
22233
+ const dbPath = path33.join(vaultPath2, ".flywheel", "state.db");
22153
22234
  const statedbExists = stateDb2 !== null;
22154
22235
  if (!statedbExists) {
22155
22236
  recommendations.push("StateDb not initialized \u2014 server needs restart");
@@ -22275,7 +22356,7 @@ async function executeRun(stateDb2, vaultPath2) {
22275
22356
  const allFiles = await collectMarkdownFiles(vaultPath2, vaultPath2, EXCLUDE_FOLDERS);
22276
22357
  let eligible = 0;
22277
22358
  for (const relativePath of allFiles) {
22278
- const fullPath = path32.join(vaultPath2, relativePath);
22359
+ const fullPath = path33.join(vaultPath2, relativePath);
22279
22360
  let content;
22280
22361
  try {
22281
22362
  content = await fs32.readFile(fullPath, "utf-8");
@@ -22333,7 +22414,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
22333
22414
  const eligible = [];
22334
22415
  let notesSkipped = 0;
22335
22416
  for (const relativePath of allFiles) {
22336
- const fullPath = path32.join(vaultPath2, relativePath);
22417
+ const fullPath = path33.join(vaultPath2, relativePath);
22337
22418
  let content;
22338
22419
  try {
22339
22420
  content = await fs32.readFile(fullPath, "utf-8");
@@ -22363,7 +22444,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
22363
22444
  match_count: result.linksAdded
22364
22445
  });
22365
22446
  if (!dryRun) {
22366
- const fullPath = path32.join(vaultPath2, relativePath);
22447
+ const fullPath = path33.join(vaultPath2, relativePath);
22367
22448
  await fs32.writeFile(fullPath, result.content, "utf-8");
22368
22449
  notesModified++;
22369
22450
  if (stateDb2) {
@@ -22596,7 +22677,7 @@ import { z as z32 } from "zod";
22596
22677
  // src/core/read/similarity.ts
22597
22678
  init_embeddings();
22598
22679
  import * as fs33 from "fs";
22599
- import * as path33 from "path";
22680
+ import * as path34 from "path";
22600
22681
  var STOP_WORDS = /* @__PURE__ */ new Set([
22601
22682
  "the",
22602
22683
  "be",
@@ -22733,7 +22814,7 @@ function extractKeyTerms(content, maxTerms = 15) {
22733
22814
  }
22734
22815
  function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
22735
22816
  const limit = options.limit ?? 10;
22736
- const absPath = path33.join(vaultPath2, sourcePath);
22817
+ const absPath = path34.join(vaultPath2, sourcePath);
22737
22818
  let content;
22738
22819
  try {
22739
22820
  content = fs33.readFileSync(absPath, "utf-8");
@@ -22875,7 +22956,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb3) {
22875
22956
  diversity: z32.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
22876
22957
  }
22877
22958
  },
22878
- async ({ path: path36, limit, diversity }) => {
22959
+ async ({ path: path37, limit, diversity }) => {
22879
22960
  const index = getIndex();
22880
22961
  const vaultPath2 = getVaultPath();
22881
22962
  const stateDb2 = getStateDb3();
@@ -22884,10 +22965,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb3) {
22884
22965
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
22885
22966
  };
22886
22967
  }
22887
- if (!index.notes.has(path36)) {
22968
+ if (!index.notes.has(path37)) {
22888
22969
  return {
22889
22970
  content: [{ type: "text", text: JSON.stringify({
22890
- error: `Note not found: ${path36}`,
22971
+ error: `Note not found: ${path37}`,
22891
22972
  hint: "Use the full relative path including .md extension"
22892
22973
  }, null, 2) }]
22893
22974
  };
@@ -22899,12 +22980,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb3) {
22899
22980
  };
22900
22981
  const useHybrid = hasEmbeddingsIndex();
22901
22982
  const method = useHybrid ? "hybrid" : "bm25";
22902
- const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path36, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path36, opts);
22983
+ const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path37, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path37, opts);
22903
22984
  return {
22904
22985
  content: [{
22905
22986
  type: "text",
22906
22987
  text: JSON.stringify({
22907
- source: path36,
22988
+ source: path37,
22908
22989
  method,
22909
22990
  count: results.length,
22910
22991
  similar: results
@@ -24136,7 +24217,7 @@ function registerVaultResources(server2, getIndex) {
24136
24217
  // src/tool-registry.ts
24137
24218
  var __trFilename = fileURLToPath(import.meta.url);
24138
24219
  var __trDirname = dirname5(__trFilename);
24139
- var trPkg = JSON.parse(readFileSync5(join18(__trDirname, "../package.json"), "utf-8"));
24220
+ var trPkg = JSON.parse(readFileSync5(join19(__trDirname, "../package.json"), "utf-8"));
24140
24221
  function applyToolGating(targetServer, categories, getDb4, registry, getVaultPath, vaultCallbacks) {
24141
24222
  let _registered = 0;
24142
24223
  let _skipped = 0;
@@ -24201,7 +24282,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
24201
24282
  let totalBytes = 0;
24202
24283
  for (const p of notePaths) {
24203
24284
  try {
24204
- totalBytes += statSync5(path34.join(vp, p)).size;
24285
+ totalBytes += statSync6(path35.join(vp, p)).size;
24205
24286
  } catch {
24206
24287
  }
24207
24288
  }
@@ -24422,7 +24503,7 @@ function registerAllTools(targetServer, ctx) {
24422
24503
  // src/index.ts
24423
24504
  var __filename = fileURLToPath2(import.meta.url);
24424
24505
  var __dirname = dirname7(__filename);
24425
- var pkg = JSON.parse(readFileSync6(join20(__dirname, "../package.json"), "utf-8"));
24506
+ var pkg = JSON.parse(readFileSync6(join21(__dirname, "../package.json"), "utf-8"));
24426
24507
  var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
24427
24508
  var resolvedVaultPath;
24428
24509
  try {
@@ -24774,7 +24855,7 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
24774
24855
  return;
24775
24856
  }
24776
24857
  for (const entry of entries) {
24777
- const fullPath = path35.join(dir, entry.name);
24858
+ const fullPath = path36.join(dir, entry.name);
24778
24859
  if (entry.isDirectory()) {
24779
24860
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
24780
24861
  await scanDir(fullPath);
@@ -24784,7 +24865,7 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
24784
24865
  if (stat5.mtimeMs > sinceMs) {
24785
24866
  events.push({
24786
24867
  type: "upsert",
24787
- path: path35.relative(vaultPath2, fullPath),
24868
+ path: path36.relative(vaultPath2, fullPath),
24788
24869
  originalEvents: []
24789
24870
  });
24790
24871
  }
@@ -24981,8 +25062,8 @@ async function runPostIndexWork(ctx) {
24981
25062
  }
24982
25063
  } catch {
24983
25064
  try {
24984
- const dir = path35.dirname(rawPath);
24985
- const base = path35.basename(rawPath);
25065
+ const dir = path36.dirname(rawPath);
25066
+ const base = path36.basename(rawPath);
24986
25067
  const resolvedDir = realpathSync(dir).replace(/\\/g, "/");
24987
25068
  for (const prefix of vaultPrefixes) {
24988
25069
  if (resolvedDir.startsWith(prefix + "/") || resolvedDir === prefix) {
@@ -25014,7 +25095,7 @@ async function runPostIndexWork(ctx) {
25014
25095
  continue;
25015
25096
  }
25016
25097
  try {
25017
- const content = await fs34.readFile(path35.join(vp, event.path), "utf-8");
25098
+ const content = await fs34.readFile(path36.join(vp, event.path), "utf-8");
25018
25099
  const hash = createHash3("sha256").update(content).digest("hex").slice(0, 16);
25019
25100
  if (lastContentHashes.get(event.path) === hash) {
25020
25101
  serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);