@velvetmonkey/flywheel-memory 2.0.19 → 2.0.20

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 +1187 -588
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -9,6 +9,41 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/core/shared/levenshtein.ts
13
+ function levenshteinDistance(a, b) {
14
+ if (a.length === 0) return b.length;
15
+ if (b.length === 0) return a.length;
16
+ const matrix = [];
17
+ for (let i = 0; i <= b.length; i++) {
18
+ matrix[i] = [i];
19
+ }
20
+ for (let j = 0; j <= a.length; j++) {
21
+ matrix[0][j] = j;
22
+ }
23
+ for (let i = 1; i <= b.length; i++) {
24
+ for (let j = 1; j <= a.length; j++) {
25
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
26
+ matrix[i][j] = matrix[i - 1][j - 1];
27
+ } else {
28
+ matrix[i][j] = Math.min(
29
+ matrix[i - 1][j - 1] + 1,
30
+ // substitution
31
+ matrix[i][j - 1] + 1,
32
+ // insertion
33
+ matrix[i - 1][j] + 1
34
+ // deletion
35
+ );
36
+ }
37
+ }
38
+ }
39
+ return matrix[b.length][a.length];
40
+ }
41
+ var init_levenshtein = __esm({
42
+ "src/core/shared/levenshtein.ts"() {
43
+ "use strict";
44
+ }
45
+ });
46
+
12
47
  // src/core/write/constants.ts
13
48
  function estimateTokens(content) {
14
49
  const str = typeof content === "string" ? content : JSON.stringify(content);
@@ -23,8 +58,8 @@ var init_constants = __esm({
23
58
  });
24
59
 
25
60
  // src/core/write/writer.ts
26
- import fs17 from "fs/promises";
27
- import path16 from "path";
61
+ import fs18 from "fs/promises";
62
+ import path17 from "path";
28
63
  import matter5 from "gray-matter";
29
64
  function isSensitivePath(filePath) {
30
65
  const normalizedPath = filePath.replace(/\\/g, "/");
@@ -263,9 +298,51 @@ function detectSectionBaseIndentation(lines, sectionStartLine, sectionEndLine) {
263
298
  }
264
299
  return "";
265
300
  }
301
+ function bumpHeadingLevels(content, parentLevel) {
302
+ const lines = content.split("\n");
303
+ let inCodeBlock = false;
304
+ let minLevel = Infinity;
305
+ for (const line of lines) {
306
+ const trimmed = line.trim();
307
+ if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
308
+ inCodeBlock = !inCodeBlock;
309
+ continue;
310
+ }
311
+ if (inCodeBlock) continue;
312
+ const match = line.match(HEADING_REGEX3);
313
+ if (match) {
314
+ const level = match[1].length;
315
+ if (level < minLevel) {
316
+ minLevel = level;
317
+ }
318
+ }
319
+ }
320
+ if (minLevel === Infinity) return content;
321
+ const bump = parentLevel + 1 - minLevel;
322
+ if (bump <= 0) return content;
323
+ inCodeBlock = false;
324
+ const result = lines.map((line) => {
325
+ const trimmed = line.trim();
326
+ if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
327
+ inCodeBlock = !inCodeBlock;
328
+ return line;
329
+ }
330
+ if (inCodeBlock) return line;
331
+ const match = line.match(HEADING_REGEX3);
332
+ if (match) {
333
+ const newLevel = Math.min(match[1].length + bump, 6);
334
+ return "#".repeat(newLevel) + " " + match[2];
335
+ }
336
+ return line;
337
+ });
338
+ return result.join("\n");
339
+ }
266
340
  function insertInSection(content, section, newContent, position, options) {
267
341
  const lines = content.split("\n");
268
- const formattedContent = newContent.trim();
342
+ let formattedContent = newContent.trim();
343
+ if (options?.bumpHeadings !== false) {
344
+ formattedContent = bumpHeadingLevels(formattedContent, section.level);
345
+ }
269
346
  if (position === "prepend") {
270
347
  if (options?.preserveListNesting) {
271
348
  const indent = detectSectionBaseIndentation(lines, section.contentStartLine, section.endLine);
@@ -345,8 +422,8 @@ function validatePath(vaultPath2, notePath) {
345
422
  if (notePath.startsWith("\\")) {
346
423
  return false;
347
424
  }
348
- const resolvedVault = path16.resolve(vaultPath2);
349
- const resolvedNote = path16.resolve(vaultPath2, notePath);
425
+ const resolvedVault = path17.resolve(vaultPath2);
426
+ const resolvedNote = path17.resolve(vaultPath2, notePath);
350
427
  return resolvedNote.startsWith(resolvedVault);
351
428
  }
352
429
  async function validatePathSecure(vaultPath2, notePath) {
@@ -374,8 +451,8 @@ async function validatePathSecure(vaultPath2, notePath) {
374
451
  reason: "Path traversal not allowed"
375
452
  };
376
453
  }
377
- const resolvedVault = path16.resolve(vaultPath2);
378
- const resolvedNote = path16.resolve(vaultPath2, notePath);
454
+ const resolvedVault = path17.resolve(vaultPath2);
455
+ const resolvedNote = path17.resolve(vaultPath2, notePath);
379
456
  if (!resolvedNote.startsWith(resolvedVault)) {
380
457
  return {
381
458
  valid: false,
@@ -389,18 +466,18 @@ async function validatePathSecure(vaultPath2, notePath) {
389
466
  };
390
467
  }
391
468
  try {
392
- const fullPath = path16.join(vaultPath2, notePath);
469
+ const fullPath = path17.join(vaultPath2, notePath);
393
470
  try {
394
- await fs17.access(fullPath);
395
- const realPath = await fs17.realpath(fullPath);
396
- const realVaultPath = await fs17.realpath(vaultPath2);
471
+ await fs18.access(fullPath);
472
+ const realPath = await fs18.realpath(fullPath);
473
+ const realVaultPath = await fs18.realpath(vaultPath2);
397
474
  if (!realPath.startsWith(realVaultPath)) {
398
475
  return {
399
476
  valid: false,
400
477
  reason: "Symlink target is outside vault"
401
478
  };
402
479
  }
403
- const relativePath = path16.relative(realVaultPath, realPath);
480
+ const relativePath = path17.relative(realVaultPath, realPath);
404
481
  if (isSensitivePath(relativePath)) {
405
482
  return {
406
483
  valid: false,
@@ -408,11 +485,11 @@ async function validatePathSecure(vaultPath2, notePath) {
408
485
  };
409
486
  }
410
487
  } catch {
411
- const parentDir = path16.dirname(fullPath);
488
+ const parentDir = path17.dirname(fullPath);
412
489
  try {
413
- await fs17.access(parentDir);
414
- const realParentPath = await fs17.realpath(parentDir);
415
- const realVaultPath = await fs17.realpath(vaultPath2);
490
+ await fs18.access(parentDir);
491
+ const realParentPath = await fs18.realpath(parentDir);
492
+ const realVaultPath = await fs18.realpath(vaultPath2);
416
493
  if (!realParentPath.startsWith(realVaultPath)) {
417
494
  return {
418
495
  valid: false,
@@ -434,8 +511,8 @@ async function readVaultFile(vaultPath2, notePath) {
434
511
  if (!validatePath(vaultPath2, notePath)) {
435
512
  throw new Error("Invalid path: path traversal not allowed");
436
513
  }
437
- const fullPath = path16.join(vaultPath2, notePath);
438
- const rawContent = await fs17.readFile(fullPath, "utf-8");
514
+ const fullPath = path17.join(vaultPath2, notePath);
515
+ const rawContent = await fs18.readFile(fullPath, "utf-8");
439
516
  const lineEnding = detectLineEnding(rawContent);
440
517
  const normalizedContent = normalizeLineEndings(rawContent);
441
518
  const parsed = matter5(normalizedContent);
@@ -483,11 +560,11 @@ async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEn
483
560
  if (!validation.valid) {
484
561
  throw new Error(`Invalid path: ${validation.reason}`);
485
562
  }
486
- const fullPath = path16.join(vaultPath2, notePath);
563
+ const fullPath = path17.join(vaultPath2, notePath);
487
564
  let output = matter5.stringify(content, frontmatter);
488
565
  output = normalizeTrailingNewline(output);
489
566
  output = convertLineEndings(output, lineEnding);
490
- await fs17.writeFile(fullPath, output, "utf-8");
567
+ await fs18.writeFile(fullPath, output, "utf-8");
491
568
  }
492
569
  function removeFromSection(content, section, pattern, mode = "first", useRegex = false) {
493
570
  const lines = content.split("\n");
@@ -555,6 +632,76 @@ function replaceInSection(content, section, search, replacement, mode = "first",
555
632
  newLines
556
633
  };
557
634
  }
635
+ function buildReplaceNotFoundDiagnostic(sectionContent, searchText, sectionName, sectionStartLine) {
636
+ const sectionLines = sectionContent.split("\n");
637
+ const sectionLineCount = sectionLines.length;
638
+ const sectionEndLine = sectionStartLine + sectionLineCount - 1;
639
+ const searchLines = searchText.split("\n");
640
+ const isMultiLine = searchLines.length > 1;
641
+ let closestMatch = null;
642
+ if (!isMultiLine) {
643
+ for (let i = 0; i < sectionLines.length; i++) {
644
+ const line = sectionLines[i].trim();
645
+ if (line === "") continue;
646
+ const dist = levenshteinDistance(searchText.trim(), line);
647
+ if (closestMatch === null || dist < closestMatch.distance) {
648
+ closestMatch = {
649
+ text: sectionLines[i],
650
+ distance: dist,
651
+ line: sectionStartLine + i
652
+ };
653
+ }
654
+ }
655
+ } else {
656
+ const windowSize = searchLines.length;
657
+ for (let i = 0; i <= sectionLines.length - windowSize; i++) {
658
+ const windowText = sectionLines.slice(i, i + windowSize).join("\n");
659
+ const dist = levenshteinDistance(searchText, windowText);
660
+ if (closestMatch === null || dist < closestMatch.distance) {
661
+ closestMatch = {
662
+ text: windowText,
663
+ distance: dist,
664
+ line: sectionStartLine + i
665
+ };
666
+ }
667
+ }
668
+ }
669
+ let lineAnalysis = null;
670
+ if (isMultiLine) {
671
+ lineAnalysis = searchLines.map((searchLine, idx) => {
672
+ const trimmedSearch = searchLine.trim();
673
+ const found = sectionLines.some((sl) => sl.includes(trimmedSearch));
674
+ return {
675
+ lineNumber: idx + 1,
676
+ searchLine: trimmedSearch,
677
+ found
678
+ };
679
+ });
680
+ }
681
+ const suggestions = [];
682
+ if (closestMatch && closestMatch.distance <= Math.max(3, Math.floor(searchText.length * 0.2))) {
683
+ suggestions.push(`Did you mean: "${closestMatch.text.trim()}"?`);
684
+ }
685
+ suggestions.push("Try using useRegex: true for pattern matching");
686
+ if (isMultiLine) {
687
+ suggestions.push("For multi-line content, try breaking into smaller replacements");
688
+ }
689
+ if (searchText !== searchText.trim() || /^\s|\s$/.test(searchText)) {
690
+ suggestions.push("Check for whitespace differences");
691
+ }
692
+ return {
693
+ sectionName,
694
+ sectionLineRange: { start: sectionStartLine, end: sectionEndLine },
695
+ sectionLineCount,
696
+ closestMatch: closestMatch ? {
697
+ text: closestMatch.text,
698
+ distance: closestMatch.distance,
699
+ line: closestMatch.line
700
+ } : null,
701
+ lineAnalysis,
702
+ suggestions
703
+ };
704
+ }
558
705
  function injectMutationMetadata(frontmatter, scoping) {
559
706
  const now = (/* @__PURE__ */ new Date()).toISOString();
560
707
  frontmatter._last_modified_at = now;
@@ -578,11 +725,12 @@ function injectMutationMetadata(frontmatter, scoping) {
578
725
  }
579
726
  return frontmatter;
580
727
  }
581
- var SENSITIVE_PATH_PATTERNS, REDOS_PATTERNS, MAX_REGEX_LENGTH, EMPTY_PLACEHOLDER_PATTERNS;
728
+ var SENSITIVE_PATH_PATTERNS, REDOS_PATTERNS, MAX_REGEX_LENGTH, EMPTY_PLACEHOLDER_PATTERNS, DiagnosticError;
582
729
  var init_writer = __esm({
583
730
  "src/core/write/writer.ts"() {
584
731
  "use strict";
585
732
  init_constants();
733
+ init_levenshtein();
586
734
  SENSITIVE_PATH_PATTERNS = [
587
735
  // Environment files (including backups, variations, and Windows ADS)
588
736
  /\.env($|\..*|~|\.swp|\.swo|:)/i,
@@ -683,6 +831,14 @@ var init_writer = __esm({
683
831
  /^\*\s*$/
684
832
  // "* " (asterisk bullet placeholder)
685
833
  ];
834
+ DiagnosticError = class extends Error {
835
+ diagnostic;
836
+ constructor(message, diagnostic) {
837
+ super(message);
838
+ this.name = "DiagnosticError";
839
+ this.diagnostic = diagnostic;
840
+ }
841
+ };
686
842
  }
687
843
  });
688
844
 
@@ -711,8 +867,8 @@ function createContext(variables = {}) {
711
867
  }
712
868
  };
713
869
  }
714
- function resolvePath(obj, path28) {
715
- const parts = path28.split(".");
870
+ function resolvePath(obj, path29) {
871
+ const parts = path29.split(".");
716
872
  let current = obj;
717
873
  for (const part of parts) {
718
874
  if (current === void 0 || current === null) {
@@ -1150,8 +1306,8 @@ __export(conditions_exports, {
1150
1306
  evaluateCondition: () => evaluateCondition,
1151
1307
  shouldStepExecute: () => shouldStepExecute
1152
1308
  });
1153
- import fs23 from "fs/promises";
1154
- import path22 from "path";
1309
+ import fs24 from "fs/promises";
1310
+ import path23 from "path";
1155
1311
  async function evaluateCondition(condition, vaultPath2, context) {
1156
1312
  const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
1157
1313
  const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
@@ -1204,9 +1360,9 @@ async function evaluateCondition(condition, vaultPath2, context) {
1204
1360
  }
1205
1361
  }
1206
1362
  async function evaluateFileExists(vaultPath2, notePath, expectExists) {
1207
- const fullPath = path22.join(vaultPath2, notePath);
1363
+ const fullPath = path23.join(vaultPath2, notePath);
1208
1364
  try {
1209
- await fs23.access(fullPath);
1365
+ await fs24.access(fullPath);
1210
1366
  return {
1211
1367
  met: expectExists,
1212
1368
  reason: expectExists ? `File exists: ${notePath}` : `File exists (expected not to): ${notePath}`
@@ -1219,9 +1375,9 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
1219
1375
  }
1220
1376
  }
1221
1377
  async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
1222
- const fullPath = path22.join(vaultPath2, notePath);
1378
+ const fullPath = path23.join(vaultPath2, notePath);
1223
1379
  try {
1224
- await fs23.access(fullPath);
1380
+ await fs24.access(fullPath);
1225
1381
  } catch {
1226
1382
  return {
1227
1383
  met: !expectExists,
@@ -1250,9 +1406,9 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
1250
1406
  }
1251
1407
  }
1252
1408
  async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
1253
- const fullPath = path22.join(vaultPath2, notePath);
1409
+ const fullPath = path23.join(vaultPath2, notePath);
1254
1410
  try {
1255
- await fs23.access(fullPath);
1411
+ await fs24.access(fullPath);
1256
1412
  } catch {
1257
1413
  return {
1258
1414
  met: !expectExists,
@@ -1281,9 +1437,9 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
1281
1437
  }
1282
1438
  }
1283
1439
  async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
1284
- const fullPath = path22.join(vaultPath2, notePath);
1440
+ const fullPath = path23.join(vaultPath2, notePath);
1285
1441
  try {
1286
- await fs23.access(fullPath);
1442
+ await fs24.access(fullPath);
1287
1443
  } catch {
1288
1444
  return {
1289
1445
  met: false,
@@ -1424,7 +1580,7 @@ var init_taskHelpers = __esm({
1424
1580
  });
1425
1581
 
1426
1582
  // src/index.ts
1427
- import * as path27 from "path";
1583
+ import * as path28 from "path";
1428
1584
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1429
1585
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1430
1586
 
@@ -1663,6 +1819,376 @@ function createEmptyNote(file) {
1663
1819
  }
1664
1820
 
1665
1821
  // src/core/read/graph.ts
1822
+ init_levenshtein();
1823
+
1824
+ // src/core/read/embeddings.ts
1825
+ import * as crypto from "crypto";
1826
+ import * as fs3 from "fs";
1827
+ import * as path2 from "path";
1828
+ var MODEL_ID = "Xenova/all-MiniLM-L6-v2";
1829
+ var EXCLUDED_DIRS2 = /* @__PURE__ */ new Set([
1830
+ ".obsidian",
1831
+ ".trash",
1832
+ ".git",
1833
+ "node_modules",
1834
+ "templates",
1835
+ ".claude",
1836
+ ".flywheel"
1837
+ ]);
1838
+ var MAX_FILE_SIZE2 = 5 * 1024 * 1024;
1839
+ var db = null;
1840
+ var pipeline = null;
1841
+ var initPromise = null;
1842
+ var embeddingCache = /* @__PURE__ */ new Map();
1843
+ var EMBEDDING_CACHE_MAX = 500;
1844
+ var entityEmbeddingsMap = /* @__PURE__ */ new Map();
1845
+ function setEmbeddingsDatabase(database) {
1846
+ db = database;
1847
+ }
1848
+ async function initEmbeddings() {
1849
+ if (pipeline) return;
1850
+ if (initPromise) return initPromise;
1851
+ initPromise = (async () => {
1852
+ try {
1853
+ const transformers = await Function("specifier", "return import(specifier)")("@huggingface/transformers");
1854
+ pipeline = await transformers.pipeline("feature-extraction", MODEL_ID, {
1855
+ dtype: "fp32"
1856
+ });
1857
+ } catch (err) {
1858
+ initPromise = null;
1859
+ if (err instanceof Error && (err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Cannot find module") || err.message.includes("ERR_MODULE_NOT_FOUND"))) {
1860
+ throw new Error(
1861
+ "Semantic search requires @huggingface/transformers. Install it with: npm install @huggingface/transformers"
1862
+ );
1863
+ }
1864
+ throw err;
1865
+ }
1866
+ })();
1867
+ return initPromise;
1868
+ }
1869
+ async function embedText(text) {
1870
+ await initEmbeddings();
1871
+ const truncated = text.slice(0, 2e3);
1872
+ const result = await pipeline(truncated, { pooling: "mean", normalize: true });
1873
+ return new Float32Array(result.data);
1874
+ }
1875
+ async function embedTextCached(text) {
1876
+ const existing = embeddingCache.get(text);
1877
+ if (existing) return existing;
1878
+ const embedding = await embedText(text);
1879
+ if (embeddingCache.size >= EMBEDDING_CACHE_MAX) {
1880
+ const firstKey = embeddingCache.keys().next().value;
1881
+ if (firstKey !== void 0) embeddingCache.delete(firstKey);
1882
+ }
1883
+ embeddingCache.set(text, embedding);
1884
+ return embedding;
1885
+ }
1886
+ function contentHash(content) {
1887
+ return crypto.createHash("md5").update(content).digest("hex");
1888
+ }
1889
+ function shouldIndexFile(filePath) {
1890
+ const parts = filePath.split("/");
1891
+ return !parts.some((part) => EXCLUDED_DIRS2.has(part));
1892
+ }
1893
+ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
1894
+ if (!db) {
1895
+ throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
1896
+ }
1897
+ await initEmbeddings();
1898
+ const files = await scanVault(vaultPath2);
1899
+ const indexable = files.filter((f) => shouldIndexFile(f.path));
1900
+ const existingHashes = /* @__PURE__ */ new Map();
1901
+ const rows = db.prepare("SELECT path, content_hash FROM note_embeddings").all();
1902
+ for (const row of rows) {
1903
+ existingHashes.set(row.path, row.content_hash);
1904
+ }
1905
+ const upsert = db.prepare(`
1906
+ INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
1907
+ VALUES (?, ?, ?, ?, ?)
1908
+ `);
1909
+ const progress = { total: indexable.length, current: 0, skipped: 0 };
1910
+ for (const file of indexable) {
1911
+ progress.current++;
1912
+ try {
1913
+ const stats = fs3.statSync(file.absolutePath);
1914
+ if (stats.size > MAX_FILE_SIZE2) {
1915
+ progress.skipped++;
1916
+ continue;
1917
+ }
1918
+ const content = fs3.readFileSync(file.absolutePath, "utf-8");
1919
+ const hash = contentHash(content);
1920
+ if (existingHashes.get(file.path) === hash) {
1921
+ progress.skipped++;
1922
+ if (onProgress) onProgress(progress);
1923
+ continue;
1924
+ }
1925
+ const embedding = await embedText(content);
1926
+ const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
1927
+ upsert.run(file.path, buf, hash, MODEL_ID, Date.now());
1928
+ } catch {
1929
+ progress.skipped++;
1930
+ }
1931
+ if (onProgress) onProgress(progress);
1932
+ }
1933
+ const currentPaths = new Set(indexable.map((f) => f.path));
1934
+ const deleteStmt = db.prepare("DELETE FROM note_embeddings WHERE path = ?");
1935
+ for (const existingPath of existingHashes.keys()) {
1936
+ if (!currentPaths.has(existingPath)) {
1937
+ deleteStmt.run(existingPath);
1938
+ }
1939
+ }
1940
+ console.error(`[Semantic] Indexed ${progress.current - progress.skipped} notes, skipped ${progress.skipped}`);
1941
+ return progress;
1942
+ }
1943
+ async function updateEmbedding(notePath, absolutePath) {
1944
+ if (!db) return;
1945
+ try {
1946
+ const content = fs3.readFileSync(absolutePath, "utf-8");
1947
+ const hash = contentHash(content);
1948
+ const existing = db.prepare("SELECT content_hash FROM note_embeddings WHERE path = ?").get(notePath);
1949
+ if (existing?.content_hash === hash) return;
1950
+ const embedding = await embedText(content);
1951
+ const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
1952
+ db.prepare(`
1953
+ INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
1954
+ VALUES (?, ?, ?, ?, ?)
1955
+ `).run(notePath, buf, hash, MODEL_ID, Date.now());
1956
+ } catch {
1957
+ }
1958
+ }
1959
+ function removeEmbedding(notePath) {
1960
+ if (!db) return;
1961
+ db.prepare("DELETE FROM note_embeddings WHERE path = ?").run(notePath);
1962
+ }
1963
+ function cosineSimilarity(a, b) {
1964
+ let dot = 0;
1965
+ let normA = 0;
1966
+ let normB = 0;
1967
+ for (let i = 0; i < a.length; i++) {
1968
+ dot += a[i] * b[i];
1969
+ normA += a[i] * a[i];
1970
+ normB += b[i] * b[i];
1971
+ }
1972
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
1973
+ if (denom === 0) return 0;
1974
+ return dot / denom;
1975
+ }
1976
+ async function semanticSearch(query, limit = 10) {
1977
+ if (!db) {
1978
+ throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
1979
+ }
1980
+ const queryEmbedding = await embedText(query);
1981
+ const rows = db.prepare("SELECT path, embedding FROM note_embeddings").all();
1982
+ const scored = [];
1983
+ for (const row of rows) {
1984
+ const noteEmbedding = new Float32Array(
1985
+ row.embedding.buffer,
1986
+ row.embedding.byteOffset,
1987
+ row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
1988
+ );
1989
+ const score = cosineSimilarity(queryEmbedding, noteEmbedding);
1990
+ const title = row.path.replace(/\.md$/, "").split("/").pop() || row.path;
1991
+ scored.push({ path: row.path, title, score: Math.round(score * 1e3) / 1e3 });
1992
+ }
1993
+ scored.sort((a, b) => b.score - a.score);
1994
+ return scored.slice(0, limit);
1995
+ }
1996
+ async function findSemanticallySimilar(sourcePath, limit = 10, excludePaths) {
1997
+ if (!db) {
1998
+ throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
1999
+ }
2000
+ const sourceRow = db.prepare("SELECT embedding FROM note_embeddings WHERE path = ?").get(sourcePath);
2001
+ if (!sourceRow) {
2002
+ return [];
2003
+ }
2004
+ const sourceEmbedding = new Float32Array(
2005
+ sourceRow.embedding.buffer,
2006
+ sourceRow.embedding.byteOffset,
2007
+ sourceRow.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
2008
+ );
2009
+ const rows = db.prepare("SELECT path, embedding FROM note_embeddings WHERE path != ?").all(sourcePath);
2010
+ const scored = [];
2011
+ for (const row of rows) {
2012
+ if (excludePaths?.has(row.path)) continue;
2013
+ const noteEmbedding = new Float32Array(
2014
+ row.embedding.buffer,
2015
+ row.embedding.byteOffset,
2016
+ row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
2017
+ );
2018
+ const score = cosineSimilarity(sourceEmbedding, noteEmbedding);
2019
+ const title = row.path.replace(/\.md$/, "").split("/").pop() || row.path;
2020
+ scored.push({ path: row.path, title, score: Math.round(score * 1e3) / 1e3 });
2021
+ }
2022
+ scored.sort((a, b) => b.score - a.score);
2023
+ return scored.slice(0, limit);
2024
+ }
2025
+ function reciprocalRankFusion(...lists) {
2026
+ const k = 60;
2027
+ const scores = /* @__PURE__ */ new Map();
2028
+ for (const list of lists) {
2029
+ for (let rank = 0; rank < list.length; rank++) {
2030
+ const item = list[rank];
2031
+ const rrfScore = 1 / (k + rank + 1);
2032
+ scores.set(item.path, (scores.get(item.path) || 0) + rrfScore);
2033
+ }
2034
+ }
2035
+ return scores;
2036
+ }
2037
+ function hasEmbeddingsIndex() {
2038
+ if (!db) return false;
2039
+ try {
2040
+ const row = db.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
2041
+ return row.count > 0;
2042
+ } catch {
2043
+ return false;
2044
+ }
2045
+ }
2046
+ function getEmbeddingsCount() {
2047
+ if (!db) return 0;
2048
+ try {
2049
+ const row = db.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
2050
+ return row.count;
2051
+ } catch {
2052
+ return 0;
2053
+ }
2054
+ }
2055
+ function loadAllNoteEmbeddings() {
2056
+ const result = /* @__PURE__ */ new Map();
2057
+ if (!db) return result;
2058
+ try {
2059
+ const rows = db.prepare("SELECT path, embedding FROM note_embeddings").all();
2060
+ for (const row of rows) {
2061
+ const embedding = new Float32Array(
2062
+ row.embedding.buffer,
2063
+ row.embedding.byteOffset,
2064
+ row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
2065
+ );
2066
+ result.set(row.path, embedding);
2067
+ }
2068
+ } catch {
2069
+ }
2070
+ return result;
2071
+ }
2072
+ function buildEntityEmbeddingText(entity, vaultPath2) {
2073
+ const parts = [entity.name, entity.name];
2074
+ if (entity.aliases.length > 0) {
2075
+ parts.push(entity.aliases.join(" "));
2076
+ }
2077
+ parts.push(entity.category);
2078
+ if (entity.path) {
2079
+ try {
2080
+ const absPath = path2.join(vaultPath2, entity.path);
2081
+ const content = fs3.readFileSync(absPath, "utf-8");
2082
+ parts.push(content.slice(0, 500));
2083
+ } catch {
2084
+ }
2085
+ }
2086
+ return parts.join(" ");
2087
+ }
2088
+ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
2089
+ if (!db) {
2090
+ throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
2091
+ }
2092
+ await initEmbeddings();
2093
+ const existingHashes = /* @__PURE__ */ new Map();
2094
+ const rows = db.prepare("SELECT entity_name, source_hash FROM entity_embeddings").all();
2095
+ for (const row of rows) {
2096
+ existingHashes.set(row.entity_name, row.source_hash);
2097
+ }
2098
+ const upsert = db.prepare(`
2099
+ INSERT OR REPLACE INTO entity_embeddings (entity_name, embedding, source_hash, model, updated_at)
2100
+ VALUES (?, ?, ?, ?, ?)
2101
+ `);
2102
+ const total = entities.size;
2103
+ let done = 0;
2104
+ let updated = 0;
2105
+ for (const [name, entity] of entities) {
2106
+ done++;
2107
+ try {
2108
+ const text = buildEntityEmbeddingText(entity, vaultPath2);
2109
+ const hash = contentHash(text);
2110
+ if (existingHashes.get(name) === hash) {
2111
+ if (onProgress) onProgress(done, total);
2112
+ continue;
2113
+ }
2114
+ const embedding = await embedTextCached(text);
2115
+ const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
2116
+ upsert.run(name, buf, hash, MODEL_ID, Date.now());
2117
+ updated++;
2118
+ } catch {
2119
+ }
2120
+ if (onProgress) onProgress(done, total);
2121
+ }
2122
+ const deleteStmt = db.prepare("DELETE FROM entity_embeddings WHERE entity_name = ?");
2123
+ for (const existingName of existingHashes.keys()) {
2124
+ if (!entities.has(existingName)) {
2125
+ deleteStmt.run(existingName);
2126
+ }
2127
+ }
2128
+ console.error(`[Semantic] Entity embeddings: ${updated} updated, ${total - updated} unchanged`);
2129
+ return updated;
2130
+ }
2131
+ async function updateEntityEmbedding(entityName, entity, vaultPath2) {
2132
+ if (!db) return;
2133
+ try {
2134
+ const text = buildEntityEmbeddingText(entity, vaultPath2);
2135
+ const hash = contentHash(text);
2136
+ const existing = db.prepare("SELECT source_hash FROM entity_embeddings WHERE entity_name = ?").get(entityName);
2137
+ if (existing?.source_hash === hash) return;
2138
+ const embedding = await embedTextCached(text);
2139
+ const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
2140
+ db.prepare(`
2141
+ INSERT OR REPLACE INTO entity_embeddings (entity_name, embedding, source_hash, model, updated_at)
2142
+ VALUES (?, ?, ?, ?, ?)
2143
+ `).run(entityName, buf, hash, MODEL_ID, Date.now());
2144
+ entityEmbeddingsMap.set(entityName, embedding);
2145
+ } catch {
2146
+ }
2147
+ }
2148
+ function findSemanticallySimilarEntities(queryEmbedding, limit, excludeEntities) {
2149
+ const scored = [];
2150
+ for (const [entityName, embedding] of entityEmbeddingsMap) {
2151
+ if (excludeEntities?.has(entityName)) continue;
2152
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
2153
+ scored.push({ entityName, similarity: Math.round(similarity * 1e3) / 1e3 });
2154
+ }
2155
+ scored.sort((a, b) => b.similarity - a.similarity);
2156
+ return scored.slice(0, limit);
2157
+ }
2158
+ function hasEntityEmbeddingsIndex() {
2159
+ return entityEmbeddingsMap.size > 0;
2160
+ }
2161
+ function loadEntityEmbeddingsToMemory() {
2162
+ if (!db) return;
2163
+ try {
2164
+ const rows = db.prepare("SELECT entity_name, embedding FROM entity_embeddings").all();
2165
+ entityEmbeddingsMap.clear();
2166
+ for (const row of rows) {
2167
+ const embedding = new Float32Array(
2168
+ row.embedding.buffer,
2169
+ row.embedding.byteOffset,
2170
+ row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
2171
+ );
2172
+ entityEmbeddingsMap.set(row.entity_name, embedding);
2173
+ }
2174
+ if (rows.length > 0) {
2175
+ console.error(`[Semantic] Loaded ${rows.length} entity embeddings into memory`);
2176
+ }
2177
+ } catch {
2178
+ }
2179
+ }
2180
+ function getEntityEmbeddingsCount() {
2181
+ if (!db) return 0;
2182
+ try {
2183
+ const row = db.prepare("SELECT COUNT(*) as count FROM entity_embeddings").get();
2184
+ return row.count;
2185
+ } catch {
2186
+ return 0;
2187
+ }
2188
+ }
2189
+
2190
+ // src/core/read/graph.ts
2191
+ init_levenshtein();
1666
2192
  var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
1667
2193
  var PARSE_CONCURRENCY = 50;
1668
2194
  var PROGRESS_INTERVAL = 100;
@@ -1690,8 +2216,8 @@ function updateIndexProgress(parsed, total) {
1690
2216
  function normalizeTarget(target) {
1691
2217
  return target.toLowerCase().replace(/\.md$/, "");
1692
2218
  }
1693
- function normalizeNotePath(path28) {
1694
- return path28.toLowerCase().replace(/\.md$/, "");
2219
+ function normalizeNotePath(path29) {
2220
+ return path29.toLowerCase().replace(/\.md$/, "");
1695
2221
  }
1696
2222
  async function buildVaultIndex(vaultPath2, options = {}) {
1697
2223
  const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
@@ -1843,39 +2369,11 @@ function findHubNotes(index, minLinks = 5) {
1843
2369
  title: note.title,
1844
2370
  backlink_count: backlinkCount,
1845
2371
  forward_link_count: forwardLinkCount,
1846
- total_connections: totalConnections
1847
- });
1848
- }
1849
- }
1850
- return hubs.sort((a, b) => b.total_connections - a.total_connections);
1851
- }
1852
- function levenshteinDistance(a, b) {
1853
- if (a.length === 0) return b.length;
1854
- if (b.length === 0) return a.length;
1855
- const matrix = [];
1856
- for (let i = 0; i <= b.length; i++) {
1857
- matrix[i] = [i];
1858
- }
1859
- for (let j = 0; j <= a.length; j++) {
1860
- matrix[0][j] = j;
1861
- }
1862
- for (let i = 1; i <= b.length; i++) {
1863
- for (let j = 1; j <= a.length; j++) {
1864
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
1865
- matrix[i][j] = matrix[i - 1][j - 1];
1866
- } else {
1867
- matrix[i][j] = Math.min(
1868
- matrix[i - 1][j - 1] + 1,
1869
- // substitution
1870
- matrix[i][j - 1] + 1,
1871
- // insertion
1872
- matrix[i - 1][j] + 1
1873
- // deletion
1874
- );
1875
- }
2372
+ total_connections: totalConnections
2373
+ });
1876
2374
  }
1877
2375
  }
1878
- return matrix[b.length][a.length];
2376
+ return hubs.sort((a, b) => b.total_connections - a.total_connections);
1879
2377
  }
1880
2378
  function findSimilarEntity(index, target) {
1881
2379
  const normalized = normalizeTarget(target);
@@ -1885,7 +2383,7 @@ function findSimilarEntity(index, target) {
1885
2383
  }
1886
2384
  const maxDist = normalizedLen <= 10 ? 1 : 2;
1887
2385
  let bestMatch;
1888
- for (const [entity, path28] of index.entities) {
2386
+ for (const [entity, path29] of index.entities) {
1889
2387
  const lenDiff = Math.abs(entity.length - normalizedLen);
1890
2388
  if (lenDiff > maxDist) {
1891
2389
  continue;
@@ -1893,7 +2391,7 @@ function findSimilarEntity(index, target) {
1893
2391
  const dist = levenshteinDistance(normalized, entity);
1894
2392
  if (dist > 0 && dist <= maxDist) {
1895
2393
  if (!bestMatch || dist < bestMatch.distance) {
1896
- bestMatch = { path: path28, entity, distance: dist };
2394
+ bestMatch = { path: path29, entity, distance: dist };
1897
2395
  if (dist === 1) {
1898
2396
  return bestMatch;
1899
2397
  }
@@ -1986,8 +2484,8 @@ function saveVaultIndexToCache(stateDb2, index) {
1986
2484
  }
1987
2485
 
1988
2486
  // src/core/read/config.ts
1989
- import * as fs3 from "fs";
1990
- import * as path2 from "path";
2487
+ import * as fs4 from "fs";
2488
+ import * as path3 from "path";
1991
2489
  import {
1992
2490
  loadFlywheelConfigFromDb,
1993
2491
  saveFlywheelConfigToDb
@@ -2030,7 +2528,7 @@ var FOLDER_PATTERNS = {
2030
2528
  function extractFolders(index) {
2031
2529
  const folders = /* @__PURE__ */ new Set();
2032
2530
  for (const notePath of index.notes.keys()) {
2033
- const dir = path2.dirname(notePath);
2531
+ const dir = path3.dirname(notePath);
2034
2532
  if (dir && dir !== ".") {
2035
2533
  const parts = dir.split(/[/\\]/);
2036
2534
  for (let i = 1; i <= parts.length; i++) {
@@ -2047,7 +2545,7 @@ function extractFolders(index) {
2047
2545
  function findMatchingFolder(folders, patterns) {
2048
2546
  const lowerPatterns = patterns.map((p) => p.toLowerCase());
2049
2547
  for (const folder of folders) {
2050
- const folderName = path2.basename(folder).toLowerCase();
2548
+ const folderName = path3.basename(folder).toLowerCase();
2051
2549
  if (lowerPatterns.includes(folderName)) {
2052
2550
  return folder;
2053
2551
  }
@@ -2060,7 +2558,7 @@ function inferConfig(index, vaultPath2) {
2060
2558
  paths: {}
2061
2559
  };
2062
2560
  if (vaultPath2) {
2063
- inferred.vault_name = path2.basename(vaultPath2);
2561
+ inferred.vault_name = path3.basename(vaultPath2);
2064
2562
  }
2065
2563
  const folders = extractFolders(index);
2066
2564
  const detectedPath = findMatchingFolder(folders, FOLDER_PATTERNS.daily_notes);
@@ -2095,9 +2593,9 @@ var TEMPLATE_PATTERNS = {
2095
2593
  };
2096
2594
  function scanTemplatesFolder(vaultPath2, templatesFolder) {
2097
2595
  const templates = {};
2098
- const absFolder = path2.join(vaultPath2, templatesFolder);
2596
+ const absFolder = path3.join(vaultPath2, templatesFolder);
2099
2597
  try {
2100
- const files = fs3.readdirSync(absFolder);
2598
+ const files = fs4.readdirSync(absFolder);
2101
2599
  for (const file of files) {
2102
2600
  for (const [type, pattern] of Object.entries(TEMPLATE_PATTERNS)) {
2103
2601
  if (pattern.test(file) && !templates[type]) {
@@ -2135,19 +2633,19 @@ function saveConfig(stateDb2, inferred, existing) {
2135
2633
  }
2136
2634
 
2137
2635
  // src/core/read/vaultRoot.ts
2138
- import * as fs4 from "fs";
2139
- import * as path3 from "path";
2636
+ import * as fs5 from "fs";
2637
+ import * as path4 from "path";
2140
2638
  var VAULT_MARKERS = [".obsidian", ".claude"];
2141
2639
  function findVaultRoot(startPath) {
2142
- let current = path3.resolve(startPath || process.cwd());
2640
+ let current = path4.resolve(startPath || process.cwd());
2143
2641
  while (true) {
2144
2642
  for (const marker of VAULT_MARKERS) {
2145
- const markerPath = path3.join(current, marker);
2146
- if (fs4.existsSync(markerPath) && fs4.statSync(markerPath).isDirectory()) {
2643
+ const markerPath = path4.join(current, marker);
2644
+ if (fs5.existsSync(markerPath) && fs5.statSync(markerPath).isDirectory()) {
2147
2645
  return current;
2148
2646
  }
2149
2647
  }
2150
- const parent = path3.dirname(current);
2648
+ const parent = path4.dirname(current);
2151
2649
  if (parent === current) {
2152
2650
  return startPath || process.cwd();
2153
2651
  }
@@ -2159,7 +2657,7 @@ function findVaultRoot(startPath) {
2159
2657
  import chokidar from "chokidar";
2160
2658
 
2161
2659
  // src/core/read/watch/pathFilter.ts
2162
- import path4 from "path";
2660
+ import path5 from "path";
2163
2661
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
2164
2662
  ".git",
2165
2663
  ".obsidian",
@@ -2257,7 +2755,7 @@ function isIgnoredDirectory(segment) {
2257
2755
  return IGNORED_DIRECTORIES.has(segment);
2258
2756
  }
2259
2757
  function hasIgnoredExtension(filePath) {
2260
- const ext = path4.extname(filePath).toLowerCase();
2758
+ const ext = path5.extname(filePath).toLowerCase();
2261
2759
  return IGNORED_EXTENSIONS.has(ext);
2262
2760
  }
2263
2761
  function matchesIgnoredPattern(filename) {
@@ -2271,7 +2769,7 @@ function normalizePath(filePath) {
2271
2769
  return normalized;
2272
2770
  }
2273
2771
  function getRelativePath(vaultPath2, filePath) {
2274
- const relative = path4.relative(vaultPath2, filePath);
2772
+ const relative = path5.relative(vaultPath2, filePath);
2275
2773
  return normalizePath(relative);
2276
2774
  }
2277
2775
  function shouldWatch(filePath, vaultPath2) {
@@ -2355,30 +2853,30 @@ var EventQueue = class {
2355
2853
  * Add a new event to the queue
2356
2854
  */
2357
2855
  push(type, rawPath) {
2358
- const path28 = normalizePath(rawPath);
2856
+ const path29 = normalizePath(rawPath);
2359
2857
  const now = Date.now();
2360
2858
  const event = {
2361
2859
  type,
2362
- path: path28,
2860
+ path: path29,
2363
2861
  timestamp: now
2364
2862
  };
2365
- let pending = this.pending.get(path28);
2863
+ let pending = this.pending.get(path29);
2366
2864
  if (!pending) {
2367
2865
  pending = {
2368
2866
  events: [],
2369
2867
  timer: null,
2370
2868
  lastEvent: now
2371
2869
  };
2372
- this.pending.set(path28, pending);
2870
+ this.pending.set(path29, pending);
2373
2871
  }
2374
2872
  pending.events.push(event);
2375
2873
  pending.lastEvent = now;
2376
- console.error(`[flywheel] QUEUE: pushed ${type} for ${path28}, pending=${this.pending.size}`);
2874
+ console.error(`[flywheel] QUEUE: pushed ${type} for ${path29}, pending=${this.pending.size}`);
2377
2875
  if (pending.timer) {
2378
2876
  clearTimeout(pending.timer);
2379
2877
  }
2380
2878
  pending.timer = setTimeout(() => {
2381
- this.flushPath(path28);
2879
+ this.flushPath(path29);
2382
2880
  }, this.config.debounceMs);
2383
2881
  if (this.pending.size >= this.config.batchSize) {
2384
2882
  this.flush();
@@ -2399,10 +2897,10 @@ var EventQueue = class {
2399
2897
  /**
2400
2898
  * Flush a single path's events
2401
2899
  */
2402
- flushPath(path28) {
2403
- const pending = this.pending.get(path28);
2900
+ flushPath(path29) {
2901
+ const pending = this.pending.get(path29);
2404
2902
  if (!pending || pending.events.length === 0) return;
2405
- console.error(`[flywheel] QUEUE: flushing ${path28}, events=${pending.events.length}`);
2903
+ console.error(`[flywheel] QUEUE: flushing ${path29}, events=${pending.events.length}`);
2406
2904
  if (pending.timer) {
2407
2905
  clearTimeout(pending.timer);
2408
2906
  pending.timer = null;
@@ -2411,7 +2909,7 @@ var EventQueue = class {
2411
2909
  if (coalescedType) {
2412
2910
  const coalesced = {
2413
2911
  type: coalescedType,
2414
- path: path28,
2912
+ path: path29,
2415
2913
  originalEvents: [...pending.events]
2416
2914
  };
2417
2915
  this.onBatch({
@@ -2419,7 +2917,7 @@ var EventQueue = class {
2419
2917
  timestamp: Date.now()
2420
2918
  });
2421
2919
  }
2422
- this.pending.delete(path28);
2920
+ this.pending.delete(path29);
2423
2921
  }
2424
2922
  /**
2425
2923
  * Flush all pending events
@@ -2431,7 +2929,7 @@ var EventQueue = class {
2431
2929
  }
2432
2930
  if (this.pending.size === 0) return;
2433
2931
  const events = [];
2434
- for (const [path28, pending] of this.pending) {
2932
+ for (const [path29, pending] of this.pending) {
2435
2933
  if (pending.timer) {
2436
2934
  clearTimeout(pending.timer);
2437
2935
  }
@@ -2439,7 +2937,7 @@ var EventQueue = class {
2439
2937
  if (coalescedType) {
2440
2938
  events.push({
2441
2939
  type: coalescedType,
2442
- path: path28,
2940
+ path: path29,
2443
2941
  originalEvents: [...pending.events]
2444
2942
  });
2445
2943
  }
@@ -2588,31 +3086,31 @@ function createVaultWatcher(options) {
2588
3086
  usePolling: config.usePolling,
2589
3087
  interval: config.usePolling ? config.pollInterval : void 0
2590
3088
  });
2591
- watcher.on("add", (path28) => {
2592
- console.error(`[flywheel] RAW EVENT: add ${path28}`);
2593
- if (shouldWatch(path28, vaultPath2)) {
2594
- console.error(`[flywheel] ACCEPTED: add ${path28}`);
2595
- eventQueue.push("add", path28);
3089
+ watcher.on("add", (path29) => {
3090
+ console.error(`[flywheel] RAW EVENT: add ${path29}`);
3091
+ if (shouldWatch(path29, vaultPath2)) {
3092
+ console.error(`[flywheel] ACCEPTED: add ${path29}`);
3093
+ eventQueue.push("add", path29);
2596
3094
  } else {
2597
- console.error(`[flywheel] FILTERED: add ${path28}`);
3095
+ console.error(`[flywheel] FILTERED: add ${path29}`);
2598
3096
  }
2599
3097
  });
2600
- watcher.on("change", (path28) => {
2601
- console.error(`[flywheel] RAW EVENT: change ${path28}`);
2602
- if (shouldWatch(path28, vaultPath2)) {
2603
- console.error(`[flywheel] ACCEPTED: change ${path28}`);
2604
- eventQueue.push("change", path28);
3098
+ watcher.on("change", (path29) => {
3099
+ console.error(`[flywheel] RAW EVENT: change ${path29}`);
3100
+ if (shouldWatch(path29, vaultPath2)) {
3101
+ console.error(`[flywheel] ACCEPTED: change ${path29}`);
3102
+ eventQueue.push("change", path29);
2605
3103
  } else {
2606
- console.error(`[flywheel] FILTERED: change ${path28}`);
3104
+ console.error(`[flywheel] FILTERED: change ${path29}`);
2607
3105
  }
2608
3106
  });
2609
- watcher.on("unlink", (path28) => {
2610
- console.error(`[flywheel] RAW EVENT: unlink ${path28}`);
2611
- if (shouldWatch(path28, vaultPath2)) {
2612
- console.error(`[flywheel] ACCEPTED: unlink ${path28}`);
2613
- eventQueue.push("unlink", path28);
3107
+ watcher.on("unlink", (path29) => {
3108
+ console.error(`[flywheel] RAW EVENT: unlink ${path29}`);
3109
+ if (shouldWatch(path29, vaultPath2)) {
3110
+ console.error(`[flywheel] ACCEPTED: unlink ${path29}`);
3111
+ eventQueue.push("unlink", path29);
2614
3112
  } else {
2615
- console.error(`[flywheel] FILTERED: unlink ${path28}`);
3113
+ console.error(`[flywheel] FILTERED: unlink ${path29}`);
2616
3114
  }
2617
3115
  });
2618
3116
  watcher.on("ready", () => {
@@ -2902,10 +3400,10 @@ function getAllFeedbackBoosts(stateDb2, folder) {
2902
3400
  for (const row of globalRows) {
2903
3401
  let accuracy;
2904
3402
  let sampleCount;
2905
- const fs28 = folderStats?.get(row.entity);
2906
- if (fs28 && fs28.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
2907
- accuracy = fs28.accuracy;
2908
- sampleCount = fs28.count;
3403
+ const fs29 = folderStats?.get(row.entity);
3404
+ if (fs29 && fs29.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
3405
+ accuracy = fs29.accuracy;
3406
+ sampleCount = fs29.count;
2909
3407
  } else {
2910
3408
  accuracy = row.correct_count / row.total;
2911
3409
  sampleCount = row.total;
@@ -2961,8 +3459,8 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
2961
3459
 
2962
3460
  // src/core/write/git.ts
2963
3461
  import { simpleGit, CheckRepoActions } from "simple-git";
2964
- import path5 from "path";
2965
- import fs5 from "fs/promises";
3462
+ import path6 from "path";
3463
+ import fs6 from "fs/promises";
2966
3464
  import {
2967
3465
  setWriteState,
2968
3466
  getWriteState,
@@ -3015,9 +3513,9 @@ function clearLastMutationCommit() {
3015
3513
  }
3016
3514
  }
3017
3515
  async function checkGitLock(vaultPath2) {
3018
- const lockPath = path5.join(vaultPath2, ".git/index.lock");
3516
+ const lockPath = path6.join(vaultPath2, ".git/index.lock");
3019
3517
  try {
3020
- const stat3 = await fs5.stat(lockPath);
3518
+ const stat3 = await fs6.stat(lockPath);
3021
3519
  const ageMs = Date.now() - stat3.mtimeMs;
3022
3520
  return {
3023
3521
  locked: true,
@@ -3038,9 +3536,9 @@ async function isGitRepo(vaultPath2) {
3038
3536
  }
3039
3537
  }
3040
3538
  async function checkLockFile(vaultPath2) {
3041
- const lockPath = path5.join(vaultPath2, ".git/index.lock");
3539
+ const lockPath = path6.join(vaultPath2, ".git/index.lock");
3042
3540
  try {
3043
- const stat3 = await fs5.stat(lockPath);
3541
+ const stat3 = await fs6.stat(lockPath);
3044
3542
  const ageMs = Date.now() - stat3.mtimeMs;
3045
3543
  return { stale: ageMs > STALE_LOCK_THRESHOLD_MS, ageMs };
3046
3544
  } catch {
@@ -3088,7 +3586,7 @@ async function commitChange(vaultPath2, filePath, messagePrefix, retryConfig = D
3088
3586
  }
3089
3587
  }
3090
3588
  await git.add(filePath);
3091
- const fileName = path5.basename(filePath);
3589
+ const fileName = path6.basename(filePath);
3092
3590
  const commitMessage = `${messagePrefix} Update ${fileName}`;
3093
3591
  const result = await git.commit(commitMessage);
3094
3592
  if (result.commit) {
@@ -3282,7 +3780,7 @@ function setHintsStateDb(stateDb2) {
3282
3780
 
3283
3781
  // src/core/shared/recency.ts
3284
3782
  import { readdir, readFile, stat } from "fs/promises";
3285
- import path6 from "path";
3783
+ import path7 from "path";
3286
3784
  import {
3287
3785
  getEntityName,
3288
3786
  recordEntityMention,
@@ -3304,9 +3802,9 @@ async function* walkMarkdownFiles(dir, baseDir) {
3304
3802
  try {
3305
3803
  const entries = await readdir(dir, { withFileTypes: true });
3306
3804
  for (const entry of entries) {
3307
- const fullPath = path6.join(dir, entry.name);
3308
- const relativePath = path6.relative(baseDir, fullPath);
3309
- const topFolder = relativePath.split(path6.sep)[0];
3805
+ const fullPath = path7.join(dir, entry.name);
3806
+ const relativePath = path7.relative(baseDir, fullPath);
3807
+ const topFolder = relativePath.split(path7.sep)[0];
3310
3808
  if (EXCLUDED_FOLDERS.has(topFolder)) {
3311
3809
  continue;
3312
3810
  }
@@ -4184,7 +4682,7 @@ function tokenize(text) {
4184
4682
 
4185
4683
  // src/core/shared/cooccurrence.ts
4186
4684
  import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
4187
- import path7 from "path";
4685
+ import path8 from "path";
4188
4686
  var DEFAULT_MIN_COOCCURRENCE = 2;
4189
4687
  var EXCLUDED_FOLDERS2 = /* @__PURE__ */ new Set([
4190
4688
  "templates",
@@ -4218,9 +4716,9 @@ async function* walkMarkdownFiles2(dir, baseDir) {
4218
4716
  try {
4219
4717
  const entries = await readdir2(dir, { withFileTypes: true });
4220
4718
  for (const entry of entries) {
4221
- const fullPath = path7.join(dir, entry.name);
4222
- const relativePath = path7.relative(baseDir, fullPath);
4223
- const topFolder = relativePath.split(path7.sep)[0];
4719
+ const fullPath = path8.join(dir, entry.name);
4720
+ const relativePath = path8.relative(baseDir, fullPath);
4721
+ const topFolder = relativePath.split(path8.sep)[0];
4224
4722
  if (EXCLUDED_FOLDERS2.has(topFolder)) {
4225
4723
  continue;
4226
4724
  }
@@ -4674,6 +5172,8 @@ var HUB_TIERS = [
4674
5172
  { threshold: 5, boost: 1 }
4675
5173
  // Small hubs
4676
5174
  ];
5175
+ var SEMANTIC_MIN_SIMILARITY = 0.3;
5176
+ var SEMANTIC_MAX_BOOST = 12;
4677
5177
  function getCrossFolderBoost(entityPath, notePath) {
4678
5178
  if (!entityPath || !notePath) return 0;
4679
5179
  const entityFolder = entityPath.split("/")[0];
@@ -4797,7 +5297,7 @@ function scoreEntity(entity, contentTokens, contentStems, config) {
4797
5297
  }
4798
5298
  return score;
4799
5299
  }
4800
- function suggestRelatedLinks(content, options = {}) {
5300
+ async function suggestRelatedLinks(content, options = {}) {
4801
5301
  const {
4802
5302
  maxSuggestions = 3,
4803
5303
  excludeLinked = true,
@@ -4944,6 +5444,62 @@ function suggestRelatedLinks(content, options = {}) {
4944
5444
  }
4945
5445
  }
4946
5446
  }
5447
+ if (content.length >= 20 && hasEntityEmbeddingsIndex()) {
5448
+ try {
5449
+ const contentEmbedding = await embedTextCached(content);
5450
+ const alreadyScoredNames = new Set(scoredEntities.map((e) => e.name));
5451
+ const semanticStrictnessMultiplier = strictness === "conservative" ? 0.6 : strictness === "aggressive" ? 1.3 : 1;
5452
+ const semanticMatches = findSemanticallySimilarEntities(
5453
+ contentEmbedding,
5454
+ (maxSuggestions || 3) * 3,
5455
+ linkedEntities
5456
+ );
5457
+ for (const match of semanticMatches) {
5458
+ if (match.similarity < SEMANTIC_MIN_SIMILARITY) continue;
5459
+ const boost = match.similarity * SEMANTIC_MAX_BOOST * semanticStrictnessMultiplier;
5460
+ const existing = scoredEntities.find((e) => e.name === match.entityName);
5461
+ if (existing) {
5462
+ existing.score += boost;
5463
+ existing.breakdown.semanticBoost = boost;
5464
+ } else if (!linkedEntities.has(match.entityName.toLowerCase())) {
5465
+ const entityWithType = entitiesWithTypes.find(
5466
+ (et) => et.entity.name === match.entityName
5467
+ );
5468
+ if (!entityWithType) continue;
5469
+ if (match.entityName.length > MAX_ENTITY_LENGTH) continue;
5470
+ if (isLikelyArticleTitle(match.entityName)) continue;
5471
+ const { entity, category } = entityWithType;
5472
+ const layerTypeBoost = TYPE_BOOST[category] || 0;
5473
+ const layerContextBoost = contextBoosts[category] || 0;
5474
+ const layerHubBoost = getHubBoost(entity);
5475
+ const layerCrossFolderBoost = notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
5476
+ const layerFeedbackAdj = feedbackBoosts.get(match.entityName) ?? 0;
5477
+ const totalScore = boost + layerTypeBoost + layerContextBoost + layerHubBoost + layerCrossFolderBoost + layerFeedbackAdj;
5478
+ if (totalScore >= adaptiveMinScore) {
5479
+ scoredEntities.push({
5480
+ name: match.entityName,
5481
+ path: entity.path || "",
5482
+ score: totalScore,
5483
+ category,
5484
+ breakdown: {
5485
+ contentMatch: 0,
5486
+ cooccurrenceBoost: 0,
5487
+ typeBoost: layerTypeBoost,
5488
+ contextBoost: layerContextBoost,
5489
+ recencyBoost: 0,
5490
+ crossFolderBoost: layerCrossFolderBoost,
5491
+ hubBoost: layerHubBoost,
5492
+ feedbackAdjustment: layerFeedbackAdj,
5493
+ semanticBoost: boost
5494
+ }
5495
+ });
5496
+ entitiesWithContentMatch.add(match.entityName);
5497
+ }
5498
+ }
5499
+ }
5500
+ } catch {
5501
+ }
5502
+ }
4947
5503
  const relevantEntities = scoredEntities.filter(
4948
5504
  (e) => entitiesWithContentMatch.has(e.name)
4949
5505
  );
@@ -5082,7 +5638,7 @@ function inferCategoryFromName(name) {
5082
5638
  }
5083
5639
  return void 0;
5084
5640
  }
5085
- function checkPreflightSimilarity(noteName) {
5641
+ async function checkPreflightSimilarity(noteName) {
5086
5642
  const result = { similarEntities: [] };
5087
5643
  if (!moduleStateDb4) return result;
5088
5644
  const exact = getEntityByName(moduleStateDb4, noteName);
@@ -5093,10 +5649,12 @@ function checkPreflightSimilarity(noteName) {
5093
5649
  category: exact.category
5094
5650
  };
5095
5651
  }
5652
+ const ftsNames = /* @__PURE__ */ new Set();
5096
5653
  try {
5097
5654
  const searchResults = searchEntitiesDb(moduleStateDb4, noteName, 5);
5098
5655
  for (const sr of searchResults) {
5099
5656
  if (sr.name.toLowerCase() === noteName.toLowerCase()) continue;
5657
+ ftsNames.add(sr.name.toLowerCase());
5100
5658
  result.similarEntities.push({
5101
5659
  name: sr.name,
5102
5660
  path: sr.path,
@@ -5106,6 +5664,27 @@ function checkPreflightSimilarity(noteName) {
5106
5664
  }
5107
5665
  } catch {
5108
5666
  }
5667
+ try {
5668
+ if (hasEntityEmbeddingsIndex()) {
5669
+ const titleEmbedding = await embedTextCached(noteName);
5670
+ const semanticMatches = findSemanticallySimilarEntities(titleEmbedding, 5);
5671
+ for (const match of semanticMatches) {
5672
+ if (match.similarity < 0.85) continue;
5673
+ if (match.entityName.toLowerCase() === noteName.toLowerCase()) continue;
5674
+ if (ftsNames.has(match.entityName.toLowerCase())) continue;
5675
+ const entity = getEntityByName(moduleStateDb4, match.entityName);
5676
+ if (entity) {
5677
+ result.similarEntities.push({
5678
+ name: entity.name,
5679
+ path: entity.path,
5680
+ category: entity.category,
5681
+ rank: match.similarity
5682
+ });
5683
+ }
5684
+ }
5685
+ }
5686
+ } catch {
5687
+ }
5109
5688
  return result;
5110
5689
  }
5111
5690
 
@@ -5131,8 +5710,8 @@ async function flushLogs() {
5131
5710
  }
5132
5711
 
5133
5712
  // src/core/read/fts5.ts
5134
- import * as fs6 from "fs";
5135
- var EXCLUDED_DIRS2 = /* @__PURE__ */ new Set([
5713
+ import * as fs7 from "fs";
5714
+ var EXCLUDED_DIRS3 = /* @__PURE__ */ new Set([
5136
5715
  ".obsidian",
5137
5716
  ".trash",
5138
5717
  ".git",
@@ -5143,7 +5722,7 @@ var EXCLUDED_DIRS2 = /* @__PURE__ */ new Set([
5143
5722
  ]);
5144
5723
  var MAX_INDEX_FILE_SIZE = 5 * 1024 * 1024;
5145
5724
  var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
5146
- var db = null;
5725
+ var db2 = null;
5147
5726
  var state = {
5148
5727
  ready: false,
5149
5728
  lastBuilt: null,
@@ -5151,14 +5730,14 @@ var state = {
5151
5730
  error: null
5152
5731
  };
5153
5732
  function setFTS5Database(database) {
5154
- db = database;
5733
+ db2 = database;
5155
5734
  try {
5156
- const row = db.prepare(
5735
+ const row = db2.prepare(
5157
5736
  "SELECT value FROM fts_metadata WHERE key = ?"
5158
5737
  ).get("last_built");
5159
5738
  if (row) {
5160
5739
  const lastBuilt = new Date(row.value);
5161
- const countRow = db.prepare("SELECT COUNT(*) as count FROM notes_fts").get();
5740
+ const countRow = db2.prepare("SELECT COUNT(*) as count FROM notes_fts").get();
5162
5741
  state = {
5163
5742
  ready: countRow.count > 0,
5164
5743
  lastBuilt,
@@ -5169,31 +5748,31 @@ function setFTS5Database(database) {
5169
5748
  } catch {
5170
5749
  }
5171
5750
  }
5172
- function shouldIndexFile(filePath) {
5751
+ function shouldIndexFile2(filePath) {
5173
5752
  const parts = filePath.split("/");
5174
- return !parts.some((part) => EXCLUDED_DIRS2.has(part));
5753
+ return !parts.some((part) => EXCLUDED_DIRS3.has(part));
5175
5754
  }
5176
5755
  async function buildFTS5Index(vaultPath2) {
5177
5756
  try {
5178
5757
  state.error = null;
5179
- if (!db) {
5758
+ if (!db2) {
5180
5759
  throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
5181
5760
  }
5182
- db.exec("DELETE FROM notes_fts");
5761
+ db2.exec("DELETE FROM notes_fts");
5183
5762
  const files = await scanVault(vaultPath2);
5184
- const indexableFiles = files.filter((f) => shouldIndexFile(f.path));
5185
- const insert = db.prepare(
5763
+ const indexableFiles = files.filter((f) => shouldIndexFile2(f.path));
5764
+ const insert = db2.prepare(
5186
5765
  "INSERT INTO notes_fts (path, title, content) VALUES (?, ?, ?)"
5187
5766
  );
5188
- const insertMany = db.transaction((filesToIndex) => {
5767
+ const insertMany = db2.transaction((filesToIndex) => {
5189
5768
  let indexed2 = 0;
5190
5769
  for (const file of filesToIndex) {
5191
5770
  try {
5192
- const stats = fs6.statSync(file.absolutePath);
5771
+ const stats = fs7.statSync(file.absolutePath);
5193
5772
  if (stats.size > MAX_INDEX_FILE_SIZE) {
5194
5773
  continue;
5195
5774
  }
5196
- const content = fs6.readFileSync(file.absolutePath, "utf-8");
5775
+ const content = fs7.readFileSync(file.absolutePath, "utf-8");
5197
5776
  const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
5198
5777
  insert.run(file.path, title, content);
5199
5778
  indexed2++;
@@ -5205,296 +5784,79 @@ async function buildFTS5Index(vaultPath2) {
5205
5784
  });
5206
5785
  const indexed = insertMany(indexableFiles);
5207
5786
  const now = /* @__PURE__ */ new Date();
5208
- db.prepare(
5787
+ db2.prepare(
5209
5788
  "INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
5210
5789
  ).run("last_built", now.toISOString());
5211
5790
  state = {
5212
5791
  ready: true,
5213
5792
  lastBuilt: now,
5214
5793
  noteCount: indexed,
5215
- error: null
5216
- };
5217
- console.error(`[FTS5] Indexed ${indexed} notes`);
5218
- return state;
5219
- } catch (err) {
5220
- state = {
5221
- ready: false,
5222
- lastBuilt: null,
5223
- noteCount: 0,
5224
- error: err instanceof Error ? err.message : String(err)
5225
- };
5226
- throw err;
5227
- }
5228
- }
5229
- function isIndexStale(_vaultPath) {
5230
- if (!db) {
5231
- return true;
5232
- }
5233
- try {
5234
- const row = db.prepare(
5235
- "SELECT value FROM fts_metadata WHERE key = ?"
5236
- ).get("last_built");
5237
- if (!row) {
5238
- return true;
5239
- }
5240
- const lastBuilt = new Date(row.value);
5241
- const age = Date.now() - lastBuilt.getTime();
5242
- return age > STALE_THRESHOLD_MS;
5243
- } catch {
5244
- return true;
5245
- }
5246
- }
5247
- function searchFTS5(_vaultPath, query, limit = 10) {
5248
- if (!db) {
5249
- throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
5250
- }
5251
- try {
5252
- const stmt = db.prepare(`
5253
- SELECT
5254
- path,
5255
- title,
5256
- snippet(notes_fts, 2, '[', ']', '...', 20) as snippet
5257
- FROM notes_fts
5258
- WHERE notes_fts MATCH ?
5259
- ORDER BY rank
5260
- LIMIT ?
5261
- `);
5262
- const results = stmt.all(query, limit);
5263
- return results;
5264
- } catch (err) {
5265
- if (err instanceof Error && err.message.includes("fts5: syntax error")) {
5266
- throw new Error(`Invalid search query: ${query}. Check FTS5 syntax.`);
5267
- }
5268
- throw err;
5269
- }
5270
- }
5271
- function getFTS5State() {
5272
- return { ...state };
5273
- }
5274
-
5275
- // src/core/read/embeddings.ts
5276
- import * as crypto from "crypto";
5277
- import * as fs7 from "fs";
5278
- var MODEL_ID = "Xenova/all-MiniLM-L6-v2";
5279
- var EXCLUDED_DIRS3 = /* @__PURE__ */ new Set([
5280
- ".obsidian",
5281
- ".trash",
5282
- ".git",
5283
- "node_modules",
5284
- "templates",
5285
- ".claude",
5286
- ".flywheel"
5287
- ]);
5288
- var MAX_FILE_SIZE2 = 5 * 1024 * 1024;
5289
- var db2 = null;
5290
- var pipeline = null;
5291
- var initPromise = null;
5292
- function setEmbeddingsDatabase(database) {
5293
- db2 = database;
5294
- }
5295
- async function initEmbeddings() {
5296
- if (pipeline) return;
5297
- if (initPromise) return initPromise;
5298
- initPromise = (async () => {
5299
- try {
5300
- const transformers = await Function("specifier", "return import(specifier)")("@huggingface/transformers");
5301
- pipeline = await transformers.pipeline("feature-extraction", MODEL_ID, {
5302
- dtype: "fp32"
5303
- });
5304
- } catch (err) {
5305
- initPromise = null;
5306
- if (err instanceof Error && (err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Cannot find module") || err.message.includes("ERR_MODULE_NOT_FOUND"))) {
5307
- throw new Error(
5308
- "Semantic search requires @huggingface/transformers. Install it with: npm install @huggingface/transformers"
5309
- );
5310
- }
5311
- throw err;
5312
- }
5313
- })();
5314
- return initPromise;
5315
- }
5316
- async function embedText(text) {
5317
- await initEmbeddings();
5318
- const truncated = text.slice(0, 2e3);
5319
- const result = await pipeline(truncated, { pooling: "mean", normalize: true });
5320
- return new Float32Array(result.data);
5321
- }
5322
- function contentHash(content) {
5323
- return crypto.createHash("md5").update(content).digest("hex");
5324
- }
5325
- function shouldIndexFile2(filePath) {
5326
- const parts = filePath.split("/");
5327
- return !parts.some((part) => EXCLUDED_DIRS3.has(part));
5328
- }
5329
- async function buildEmbeddingsIndex(vaultPath2, onProgress) {
5330
- if (!db2) {
5331
- throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
5332
- }
5333
- await initEmbeddings();
5334
- const files = await scanVault(vaultPath2);
5335
- const indexable = files.filter((f) => shouldIndexFile2(f.path));
5336
- const existingHashes = /* @__PURE__ */ new Map();
5337
- const rows = db2.prepare("SELECT path, content_hash FROM note_embeddings").all();
5338
- for (const row of rows) {
5339
- existingHashes.set(row.path, row.content_hash);
5340
- }
5341
- const upsert = db2.prepare(`
5342
- INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
5343
- VALUES (?, ?, ?, ?, ?)
5344
- `);
5345
- const progress = { total: indexable.length, current: 0, skipped: 0 };
5346
- for (const file of indexable) {
5347
- progress.current++;
5348
- try {
5349
- const stats = fs7.statSync(file.absolutePath);
5350
- if (stats.size > MAX_FILE_SIZE2) {
5351
- progress.skipped++;
5352
- continue;
5353
- }
5354
- const content = fs7.readFileSync(file.absolutePath, "utf-8");
5355
- const hash = contentHash(content);
5356
- if (existingHashes.get(file.path) === hash) {
5357
- progress.skipped++;
5358
- if (onProgress) onProgress(progress);
5359
- continue;
5360
- }
5361
- const embedding = await embedText(content);
5362
- const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
5363
- upsert.run(file.path, buf, hash, MODEL_ID, Date.now());
5364
- } catch {
5365
- progress.skipped++;
5366
- }
5367
- if (onProgress) onProgress(progress);
5368
- }
5369
- const currentPaths = new Set(indexable.map((f) => f.path));
5370
- const deleteStmt = db2.prepare("DELETE FROM note_embeddings WHERE path = ?");
5371
- for (const existingPath of existingHashes.keys()) {
5372
- if (!currentPaths.has(existingPath)) {
5373
- deleteStmt.run(existingPath);
5374
- }
5375
- }
5376
- console.error(`[Semantic] Indexed ${progress.current - progress.skipped} notes, skipped ${progress.skipped}`);
5377
- return progress;
5378
- }
5379
- async function updateEmbedding(notePath, absolutePath) {
5380
- if (!db2) return;
5381
- try {
5382
- const content = fs7.readFileSync(absolutePath, "utf-8");
5383
- const hash = contentHash(content);
5384
- const existing = db2.prepare("SELECT content_hash FROM note_embeddings WHERE path = ?").get(notePath);
5385
- if (existing?.content_hash === hash) return;
5386
- const embedding = await embedText(content);
5387
- const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
5388
- db2.prepare(`
5389
- INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
5390
- VALUES (?, ?, ?, ?, ?)
5391
- `).run(notePath, buf, hash, MODEL_ID, Date.now());
5392
- } catch {
5393
- }
5394
- }
5395
- function removeEmbedding(notePath) {
5396
- if (!db2) return;
5397
- db2.prepare("DELETE FROM note_embeddings WHERE path = ?").run(notePath);
5398
- }
5399
- function cosineSimilarity(a, b) {
5400
- let dot = 0;
5401
- let normA = 0;
5402
- let normB = 0;
5403
- for (let i = 0; i < a.length; i++) {
5404
- dot += a[i] * b[i];
5405
- normA += a[i] * a[i];
5406
- normB += b[i] * b[i];
5407
- }
5408
- const denom = Math.sqrt(normA) * Math.sqrt(normB);
5409
- if (denom === 0) return 0;
5410
- return dot / denom;
5411
- }
5412
- async function semanticSearch(query, limit = 10) {
5413
- if (!db2) {
5414
- throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
5415
- }
5416
- const queryEmbedding = await embedText(query);
5417
- const rows = db2.prepare("SELECT path, embedding FROM note_embeddings").all();
5418
- const scored = [];
5419
- for (const row of rows) {
5420
- const noteEmbedding = new Float32Array(
5421
- row.embedding.buffer,
5422
- row.embedding.byteOffset,
5423
- row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
5424
- );
5425
- const score = cosineSimilarity(queryEmbedding, noteEmbedding);
5426
- const title = row.path.replace(/\.md$/, "").split("/").pop() || row.path;
5427
- scored.push({ path: row.path, title, score: Math.round(score * 1e3) / 1e3 });
5794
+ error: null
5795
+ };
5796
+ console.error(`[FTS5] Indexed ${indexed} notes`);
5797
+ return state;
5798
+ } catch (err) {
5799
+ state = {
5800
+ ready: false,
5801
+ lastBuilt: null,
5802
+ noteCount: 0,
5803
+ error: err instanceof Error ? err.message : String(err)
5804
+ };
5805
+ throw err;
5428
5806
  }
5429
- scored.sort((a, b) => b.score - a.score);
5430
- return scored.slice(0, limit);
5431
5807
  }
5432
- async function findSemanticallySimilar(sourcePath, limit = 10, excludePaths) {
5808
+ function isIndexStale(_vaultPath) {
5433
5809
  if (!db2) {
5434
- throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
5435
- }
5436
- const sourceRow = db2.prepare("SELECT embedding FROM note_embeddings WHERE path = ?").get(sourcePath);
5437
- if (!sourceRow) {
5438
- return [];
5439
- }
5440
- const sourceEmbedding = new Float32Array(
5441
- sourceRow.embedding.buffer,
5442
- sourceRow.embedding.byteOffset,
5443
- sourceRow.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
5444
- );
5445
- const rows = db2.prepare("SELECT path, embedding FROM note_embeddings WHERE path != ?").all(sourcePath);
5446
- const scored = [];
5447
- for (const row of rows) {
5448
- if (excludePaths?.has(row.path)) continue;
5449
- const noteEmbedding = new Float32Array(
5450
- row.embedding.buffer,
5451
- row.embedding.byteOffset,
5452
- row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
5453
- );
5454
- const score = cosineSimilarity(sourceEmbedding, noteEmbedding);
5455
- const title = row.path.replace(/\.md$/, "").split("/").pop() || row.path;
5456
- scored.push({ path: row.path, title, score: Math.round(score * 1e3) / 1e3 });
5457
- }
5458
- scored.sort((a, b) => b.score - a.score);
5459
- return scored.slice(0, limit);
5460
- }
5461
- function reciprocalRankFusion(...lists) {
5462
- const k = 60;
5463
- const scores = /* @__PURE__ */ new Map();
5464
- for (const list of lists) {
5465
- for (let rank = 0; rank < list.length; rank++) {
5466
- const item = list[rank];
5467
- const rrfScore = 1 / (k + rank + 1);
5468
- scores.set(item.path, (scores.get(item.path) || 0) + rrfScore);
5469
- }
5810
+ return true;
5470
5811
  }
5471
- return scores;
5472
- }
5473
- function hasEmbeddingsIndex() {
5474
- if (!db2) return false;
5475
5812
  try {
5476
- const row = db2.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
5477
- return row.count > 0;
5813
+ const row = db2.prepare(
5814
+ "SELECT value FROM fts_metadata WHERE key = ?"
5815
+ ).get("last_built");
5816
+ if (!row) {
5817
+ return true;
5818
+ }
5819
+ const lastBuilt = new Date(row.value);
5820
+ const age = Date.now() - lastBuilt.getTime();
5821
+ return age > STALE_THRESHOLD_MS;
5478
5822
  } catch {
5479
- return false;
5823
+ return true;
5480
5824
  }
5481
5825
  }
5482
- function getEmbeddingsCount() {
5483
- if (!db2) return 0;
5826
+ function searchFTS5(_vaultPath, query, limit = 10) {
5827
+ if (!db2) {
5828
+ throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
5829
+ }
5484
5830
  try {
5485
- const row = db2.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
5486
- return row.count;
5487
- } catch {
5488
- return 0;
5831
+ const stmt = db2.prepare(`
5832
+ SELECT
5833
+ path,
5834
+ title,
5835
+ snippet(notes_fts, 2, '[', ']', '...', 20) as snippet
5836
+ FROM notes_fts
5837
+ WHERE notes_fts MATCH ?
5838
+ ORDER BY rank
5839
+ LIMIT ?
5840
+ `);
5841
+ const results = stmt.all(query, limit);
5842
+ return results;
5843
+ } catch (err) {
5844
+ if (err instanceof Error && err.message.includes("fts5: syntax error")) {
5845
+ throw new Error(`Invalid search query: ${query}. Check FTS5 syntax.`);
5846
+ }
5847
+ throw err;
5489
5848
  }
5490
5849
  }
5850
+ function getFTS5State() {
5851
+ return { ...state };
5852
+ }
5491
5853
 
5492
5854
  // src/index.ts
5493
- import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId } from "@velvetmonkey/vault-core";
5855
+ import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb2 } from "@velvetmonkey/vault-core";
5494
5856
 
5495
5857
  // src/tools/read/graph.ts
5496
5858
  import * as fs8 from "fs";
5497
- import * as path8 from "path";
5859
+ import * as path9 from "path";
5498
5860
  import { z } from "zod";
5499
5861
 
5500
5862
  // src/core/read/constants.ts
@@ -5778,7 +6140,7 @@ function requireIndex() {
5778
6140
  // src/tools/read/graph.ts
5779
6141
  async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
5780
6142
  try {
5781
- const fullPath = path8.join(vaultPath2, sourcePath);
6143
+ const fullPath = path9.join(vaultPath2, sourcePath);
5782
6144
  const content = await fs8.promises.readFile(fullPath, "utf-8");
5783
6145
  const lines = content.split("\n");
5784
6146
  const startLine = Math.max(0, line - 1 - contextLines);
@@ -6046,7 +6408,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
6046
6408
  suggestions: matches
6047
6409
  };
6048
6410
  if (detail) {
6049
- const scored = suggestRelatedLinks(text, {
6411
+ const scored = await suggestRelatedLinks(text, {
6050
6412
  detail: true,
6051
6413
  maxSuggestions: limit,
6052
6414
  strictness: "balanced"
@@ -6082,14 +6444,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
6082
6444
  };
6083
6445
  function findSimilarEntity2(target, entities) {
6084
6446
  const targetLower = target.toLowerCase();
6085
- for (const [name, path28] of entities) {
6447
+ for (const [name, path29] of entities) {
6086
6448
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
6087
- return path28;
6449
+ return path29;
6088
6450
  }
6089
6451
  }
6090
- for (const [name, path28] of entities) {
6452
+ for (const [name, path29] of entities) {
6091
6453
  if (name.includes(targetLower) || targetLower.includes(name)) {
6092
- return path28;
6454
+ return path29;
6093
6455
  }
6094
6456
  }
6095
6457
  return void 0;
@@ -6665,8 +7027,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6665
7027
  daily_counts: z3.record(z3.number())
6666
7028
  }).describe("Activity summary for the last 7 days")
6667
7029
  };
6668
- function isPeriodicNote(path28) {
6669
- const filename = path28.split("/").pop() || "";
7030
+ function isPeriodicNote(path29) {
7031
+ const filename = path29.split("/").pop() || "";
6670
7032
  const nameWithoutExt = filename.replace(/\.md$/, "");
6671
7033
  const patterns = [
6672
7034
  /^\d{4}-\d{2}-\d{2}$/,
@@ -6681,7 +7043,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6681
7043
  // YYYY (yearly)
6682
7044
  ];
6683
7045
  const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
6684
- const folder = path28.split("/")[0]?.toLowerCase() || "";
7046
+ const folder = path29.split("/")[0]?.toLowerCase() || "";
6685
7047
  return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
6686
7048
  }
6687
7049
  server2.registerTool(
@@ -7007,7 +7369,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
7007
7369
 
7008
7370
  // src/tools/read/system.ts
7009
7371
  import * as fs10 from "fs";
7010
- import * as path9 from "path";
7372
+ import * as path10 from "path";
7011
7373
  import { z as z5 } from "zod";
7012
7374
  import { scanVaultEntities as scanVaultEntities2 } from "@velvetmonkey/vault-core";
7013
7375
  function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
@@ -7240,7 +7602,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
7240
7602
  continue;
7241
7603
  }
7242
7604
  try {
7243
- const fullPath = path9.join(vaultPath2, note.path);
7605
+ const fullPath = path10.join(vaultPath2, note.path);
7244
7606
  const content = await fs10.promises.readFile(fullPath, "utf-8");
7245
7607
  const lines = content.split("\n");
7246
7608
  for (let i = 0; i < lines.length; i++) {
@@ -7356,7 +7718,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
7356
7718
  let wordCount;
7357
7719
  if (include_word_count) {
7358
7720
  try {
7359
- const fullPath = path9.join(vaultPath2, resolvedPath);
7721
+ const fullPath = path10.join(vaultPath2, resolvedPath);
7360
7722
  const content = await fs10.promises.readFile(fullPath, "utf-8");
7361
7723
  wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
7362
7724
  } catch {
@@ -7460,7 +7822,7 @@ import { z as z6 } from "zod";
7460
7822
 
7461
7823
  // src/tools/read/structure.ts
7462
7824
  import * as fs11 from "fs";
7463
- import * as path10 from "path";
7825
+ import * as path11 from "path";
7464
7826
  var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
7465
7827
  function extractHeadings(content) {
7466
7828
  const lines = content.split("\n");
@@ -7514,7 +7876,7 @@ function buildSections(headings, totalLines) {
7514
7876
  async function getNoteStructure(index, notePath, vaultPath2) {
7515
7877
  const note = index.notes.get(notePath);
7516
7878
  if (!note) return null;
7517
- const absolutePath = path10.join(vaultPath2, notePath);
7879
+ const absolutePath = path11.join(vaultPath2, notePath);
7518
7880
  let content;
7519
7881
  try {
7520
7882
  content = await fs11.promises.readFile(absolutePath, "utf-8");
@@ -7537,7 +7899,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
7537
7899
  async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
7538
7900
  const note = index.notes.get(notePath);
7539
7901
  if (!note) return null;
7540
- const absolutePath = path10.join(vaultPath2, notePath);
7902
+ const absolutePath = path11.join(vaultPath2, notePath);
7541
7903
  let content;
7542
7904
  try {
7543
7905
  content = await fs11.promises.readFile(absolutePath, "utf-8");
@@ -7579,7 +7941,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
7579
7941
  const results = [];
7580
7942
  for (const note of index.notes.values()) {
7581
7943
  if (folder && !note.path.startsWith(folder)) continue;
7582
- const absolutePath = path10.join(vaultPath2, note.path);
7944
+ const absolutePath = path11.join(vaultPath2, note.path);
7583
7945
  let content;
7584
7946
  try {
7585
7947
  content = await fs11.promises.readFile(absolutePath, "utf-8");
@@ -7603,7 +7965,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
7603
7965
 
7604
7966
  // src/tools/read/tasks.ts
7605
7967
  import * as fs12 from "fs";
7606
- import * as path11 from "path";
7968
+ import * as path12 from "path";
7607
7969
  var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
7608
7970
  var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
7609
7971
  var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
@@ -7672,7 +8034,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
7672
8034
  const allTasks = [];
7673
8035
  for (const note of index.notes.values()) {
7674
8036
  if (folder && !note.path.startsWith(folder)) continue;
7675
- const absolutePath = path11.join(vaultPath2, note.path);
8037
+ const absolutePath = path12.join(vaultPath2, note.path);
7676
8038
  const tasks = await extractTasksFromNote(note.path, absolutePath);
7677
8039
  allTasks.push(...tasks);
7678
8040
  }
@@ -7703,7 +8065,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
7703
8065
  async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
7704
8066
  const note = index.notes.get(notePath);
7705
8067
  if (!note) return null;
7706
- const absolutePath = path11.join(vaultPath2, notePath);
8068
+ const absolutePath = path12.join(vaultPath2, notePath);
7707
8069
  let tasks = await extractTasksFromNote(notePath, absolutePath);
7708
8070
  if (excludeTags.length > 0) {
7709
8071
  tasks = tasks.filter(
@@ -7734,18 +8096,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
7734
8096
  include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
7735
8097
  }
7736
8098
  },
7737
- async ({ path: path28, include_content }) => {
8099
+ async ({ path: path29, include_content }) => {
7738
8100
  const index = getIndex();
7739
8101
  const vaultPath2 = getVaultPath();
7740
- const result = await getNoteStructure(index, path28, vaultPath2);
8102
+ const result = await getNoteStructure(index, path29, vaultPath2);
7741
8103
  if (!result) {
7742
8104
  return {
7743
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path28 }, null, 2) }]
8105
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path29 }, null, 2) }]
7744
8106
  };
7745
8107
  }
7746
8108
  if (include_content) {
7747
8109
  for (const section of result.sections) {
7748
- const sectionResult = await getSectionContent(index, path28, section.heading.text, vaultPath2, true);
8110
+ const sectionResult = await getSectionContent(index, path29, section.heading.text, vaultPath2, true);
7749
8111
  if (sectionResult) {
7750
8112
  section.content = sectionResult.content;
7751
8113
  }
@@ -7767,15 +8129,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
7767
8129
  include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
7768
8130
  }
7769
8131
  },
7770
- async ({ path: path28, heading, include_subheadings }) => {
8132
+ async ({ path: path29, heading, include_subheadings }) => {
7771
8133
  const index = getIndex();
7772
8134
  const vaultPath2 = getVaultPath();
7773
- const result = await getSectionContent(index, path28, heading, vaultPath2, include_subheadings);
8135
+ const result = await getSectionContent(index, path29, heading, vaultPath2, include_subheadings);
7774
8136
  if (!result) {
7775
8137
  return {
7776
8138
  content: [{ type: "text", text: JSON.stringify({
7777
8139
  error: "Section not found",
7778
- path: path28,
8140
+ path: path29,
7779
8141
  heading
7780
8142
  }, null, 2) }]
7781
8143
  };
@@ -7829,16 +8191,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
7829
8191
  offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
7830
8192
  }
7831
8193
  },
7832
- async ({ path: path28, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
8194
+ async ({ path: path29, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
7833
8195
  const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
7834
8196
  const index = getIndex();
7835
8197
  const vaultPath2 = getVaultPath();
7836
8198
  const config = getConfig();
7837
- if (path28) {
7838
- const result2 = await getTasksFromNote(index, path28, vaultPath2, config.exclude_task_tags || []);
8199
+ if (path29) {
8200
+ const result2 = await getTasksFromNote(index, path29, vaultPath2, config.exclude_task_tags || []);
7839
8201
  if (!result2) {
7840
8202
  return {
7841
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path28 }, null, 2) }]
8203
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path29 }, null, 2) }]
7842
8204
  };
7843
8205
  }
7844
8206
  let filtered = result2;
@@ -7848,7 +8210,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
7848
8210
  const paged2 = filtered.slice(offset, offset + limit);
7849
8211
  return {
7850
8212
  content: [{ type: "text", text: JSON.stringify({
7851
- path: path28,
8213
+ path: path29,
7852
8214
  total_count: filtered.length,
7853
8215
  returned_count: paged2.length,
7854
8216
  open: result2.filter((t) => t.status === "open").length,
@@ -7964,7 +8326,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
7964
8326
  // src/tools/read/migrations.ts
7965
8327
  import { z as z7 } from "zod";
7966
8328
  import * as fs13 from "fs/promises";
7967
- import * as path12 from "path";
8329
+ import * as path13 from "path";
7968
8330
  import matter2 from "gray-matter";
7969
8331
  function getNotesInFolder(index, folder) {
7970
8332
  const notes = [];
@@ -7977,7 +8339,7 @@ function getNotesInFolder(index, folder) {
7977
8339
  return notes;
7978
8340
  }
7979
8341
  async function readFileContent(notePath, vaultPath2) {
7980
- const fullPath = path12.join(vaultPath2, notePath);
8342
+ const fullPath = path13.join(vaultPath2, notePath);
7981
8343
  try {
7982
8344
  return await fs13.readFile(fullPath, "utf-8");
7983
8345
  } catch {
@@ -7985,7 +8347,7 @@ async function readFileContent(notePath, vaultPath2) {
7985
8347
  }
7986
8348
  }
7987
8349
  async function writeFileContent(notePath, vaultPath2, content) {
7988
- const fullPath = path12.join(vaultPath2, notePath);
8350
+ const fullPath = path13.join(vaultPath2, notePath);
7989
8351
  try {
7990
8352
  await fs13.writeFile(fullPath, content, "utf-8");
7991
8353
  return true;
@@ -8166,7 +8528,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
8166
8528
 
8167
8529
  // src/tools/read/graphAnalysis.ts
8168
8530
  import fs14 from "node:fs";
8169
- import path13 from "node:path";
8531
+ import path14 from "node:path";
8170
8532
  import { z as z8 } from "zod";
8171
8533
 
8172
8534
  // src/tools/read/schema.ts
@@ -8725,9 +9087,9 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
8725
9087
  "graph_analysis",
8726
9088
  {
8727
9089
  title: "Graph Analysis",
8728
- description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (disconnected content)\n- "dead_ends": Notes with backlinks but no outgoing links\n- "sources": Notes with outgoing links but no backlinks\n- "hubs": Highly connected notes (many links to/from)\n- "stale": Important notes (by backlink count) not recently modified\n- "immature": Notes scored by maturity (word count, links, frontmatter completeness, backlinks)\n- "evolution": Graph topology metrics over time (avg_degree, cluster_count, etc.)\n- "emerging_hubs": Entities growing fastest in connection count\n\nExample: graph_analysis({ analysis: "hubs", limit: 10 })\nExample: graph_analysis({ analysis: "stale", days: 30, min_backlinks: 3 })\nExample: graph_analysis({ analysis: "immature", folder: "projects", limit: 20 })\nExample: graph_analysis({ analysis: "evolution", days: 30 })\nExample: graph_analysis({ analysis: "emerging_hubs", days: 30 })',
9090
+ description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (disconnected content)\n- "dead_ends": Notes with backlinks but no outgoing links\n- "sources": Notes with outgoing links but no backlinks\n- "hubs": Highly connected notes (many links to/from)\n- "stale": Important notes (by backlink count) not recently modified\n- "immature": Notes scored by maturity (word count, links, frontmatter completeness, backlinks)\n- "evolution": Graph topology metrics over time (avg_degree, cluster_count, etc.)\n- "emerging_hubs": Entities growing fastest in connection count\n- "semantic_clusters": Group notes by embedding similarity (requires init_semantic)\n- "semantic_bridges": Find semantically similar but unlinked notes (highest-value link suggestions)\n\nExample: graph_analysis({ analysis: "hubs", limit: 10 })\nExample: graph_analysis({ analysis: "stale", days: 30, min_backlinks: 3 })\nExample: graph_analysis({ analysis: "immature", folder: "projects", limit: 20 })\nExample: graph_analysis({ analysis: "evolution", days: 30 })\nExample: graph_analysis({ analysis: "emerging_hubs", days: 30 })\nExample: graph_analysis({ analysis: "semantic_clusters", limit: 20 })\nExample: graph_analysis({ analysis: "semantic_bridges", limit: 20 })',
8729
9091
  inputSchema: {
8730
- analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale", "immature", "evolution", "emerging_hubs"]).describe("Type of graph analysis to perform"),
9092
+ analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale", "immature", "evolution", "emerging_hubs", "semantic_clusters", "semantic_bridges"]).describe("Type of graph analysis to perform"),
8731
9093
  folder: z8.string().optional().describe("Limit to notes in this folder (orphans, dead_ends, sources)"),
8732
9094
  min_links: z8.coerce.number().default(5).describe("Minimum total connections for hubs"),
8733
9095
  min_backlinks: z8.coerce.number().default(1).describe("Minimum backlinks (dead_ends, stale)"),
@@ -8835,7 +9197,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
8835
9197
  const scored = allNotes.map((note) => {
8836
9198
  let wordCount = 0;
8837
9199
  try {
8838
- const content = fs14.readFileSync(path13.join(vaultPath2, note.path), "utf-8");
9200
+ const content = fs14.readFileSync(path14.join(vaultPath2, note.path), "utf-8");
8839
9201
  const body = content.replace(/^---[\s\S]*?---\n?/, "");
8840
9202
  wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
8841
9203
  } catch {
@@ -8922,6 +9284,128 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
8922
9284
  }, null, 2) }]
8923
9285
  };
8924
9286
  }
9287
+ case "semantic_clusters": {
9288
+ if (!hasEmbeddingsIndex()) {
9289
+ return {
9290
+ content: [{ type: "text", text: JSON.stringify({
9291
+ error: "Note embeddings not available. Run init_semantic first."
9292
+ }, null, 2) }]
9293
+ };
9294
+ }
9295
+ const embeddings = loadAllNoteEmbeddings();
9296
+ const CLUSTER_THRESHOLD = 0.6;
9297
+ const unassigned = new Set(embeddings.keys());
9298
+ const clusters = [];
9299
+ while (unassigned.size > 0) {
9300
+ const seedPath = unassigned.values().next().value;
9301
+ unassigned.delete(seedPath);
9302
+ const seedEmb = embeddings.get(seedPath);
9303
+ const clusterNotes = [
9304
+ { path: seedPath, title: seedPath.replace(/\.md$/, "").split("/").pop() || seedPath }
9305
+ ];
9306
+ for (const candidatePath of [...unassigned]) {
9307
+ const candidateEmb = embeddings.get(candidatePath);
9308
+ const sim = cosineSimilarity(seedEmb, candidateEmb);
9309
+ if (sim >= CLUSTER_THRESHOLD) {
9310
+ unassigned.delete(candidatePath);
9311
+ clusterNotes.push({
9312
+ path: candidatePath,
9313
+ title: candidatePath.replace(/\.md$/, "").split("/").pop() || candidatePath
9314
+ });
9315
+ }
9316
+ }
9317
+ if (clusterNotes.length >= 2) {
9318
+ const commonPrefix = clusterNotes[0].path.split("/").slice(0, -1).join("/");
9319
+ const label = commonPrefix || clusterNotes[0].title;
9320
+ clusters.push({ label, notes: clusterNotes });
9321
+ }
9322
+ }
9323
+ clusters.sort((a, b) => b.notes.length - a.notes.length);
9324
+ const paginated = clusters.slice(offset, offset + limit);
9325
+ return {
9326
+ content: [{ type: "text", text: JSON.stringify({
9327
+ analysis: "semantic_clusters",
9328
+ total_clusters: clusters.length,
9329
+ returned_count: paginated.length,
9330
+ clusters: paginated.map((c) => ({
9331
+ label: c.label,
9332
+ note_count: c.notes.length,
9333
+ notes: c.notes
9334
+ }))
9335
+ }, null, 2) }]
9336
+ };
9337
+ }
9338
+ case "semantic_bridges": {
9339
+ if (!hasEmbeddingsIndex()) {
9340
+ return {
9341
+ content: [{ type: "text", text: JSON.stringify({
9342
+ error: "Note embeddings not available. Run init_semantic first."
9343
+ }, null, 2) }]
9344
+ };
9345
+ }
9346
+ const embeddings = loadAllNoteEmbeddings();
9347
+ const BRIDGE_SIM_THRESHOLD = 0.5;
9348
+ const linkedPairs = /* @__PURE__ */ new Set();
9349
+ for (const note of index.notes.values()) {
9350
+ for (const link of note.outlinks) {
9351
+ const targetPath = resolveTarget(index, link.target);
9352
+ if (targetPath) {
9353
+ linkedPairs.add(`${note.path}|${targetPath}`);
9354
+ linkedPairs.add(`${targetPath}|${note.path}`);
9355
+ }
9356
+ }
9357
+ }
9358
+ const twoHopConnected = (pathA, pathB) => {
9359
+ if (linkedPairs.has(`${pathA}|${pathB}`)) return true;
9360
+ const noteA = index.notes.get(pathA);
9361
+ const noteB = index.notes.get(pathB);
9362
+ if (!noteA || !noteB) return false;
9363
+ const neighborsA = /* @__PURE__ */ new Set();
9364
+ for (const link of noteA.outlinks) {
9365
+ const resolved = resolveTarget(index, link.target);
9366
+ if (resolved) neighborsA.add(resolved);
9367
+ }
9368
+ const backlinksA = getBacklinksForNote(index, pathA);
9369
+ for (const bl of backlinksA) {
9370
+ neighborsA.add(bl.source);
9371
+ }
9372
+ for (const link of noteB.outlinks) {
9373
+ const resolved = resolveTarget(index, link.target);
9374
+ if (resolved && neighborsA.has(resolved)) return true;
9375
+ }
9376
+ const backlinksB = getBacklinksForNote(index, pathB);
9377
+ for (const bl of backlinksB) {
9378
+ if (neighborsA.has(bl.source)) return true;
9379
+ }
9380
+ return false;
9381
+ };
9382
+ const paths = [...embeddings.keys()];
9383
+ const bridges = [];
9384
+ for (let i = 0; i < paths.length; i++) {
9385
+ const embA = embeddings.get(paths[i]);
9386
+ for (let j = i + 1; j < paths.length; j++) {
9387
+ const sim = cosineSimilarity(embA, embeddings.get(paths[j]));
9388
+ if (sim >= BRIDGE_SIM_THRESHOLD && !twoHopConnected(paths[i], paths[j])) {
9389
+ bridges.push({
9390
+ noteA: { path: paths[i], title: paths[i].replace(/\.md$/, "").split("/").pop() || paths[i] },
9391
+ noteB: { path: paths[j], title: paths[j].replace(/\.md$/, "").split("/").pop() || paths[j] },
9392
+ similarity: Math.round(sim * 1e3) / 1e3
9393
+ });
9394
+ }
9395
+ }
9396
+ }
9397
+ bridges.sort((a, b) => b.similarity - a.similarity);
9398
+ const paginatedBridges = bridges.slice(offset, offset + limit);
9399
+ return {
9400
+ content: [{ type: "text", text: JSON.stringify({
9401
+ analysis: "semantic_bridges",
9402
+ total_bridges: bridges.length,
9403
+ returned_count: paginatedBridges.length,
9404
+ description: "Notes with high semantic similarity but no direct or 2-hop link path. These represent the highest-value missing link suggestions.",
9405
+ bridges: paginatedBridges
9406
+ }, null, 2) }]
9407
+ };
9408
+ }
8925
9409
  }
8926
9410
  }
8927
9411
  );
@@ -9279,13 +9763,13 @@ import { z as z10 } from "zod";
9279
9763
 
9280
9764
  // src/tools/read/bidirectional.ts
9281
9765
  import * as fs15 from "fs/promises";
9282
- import * as path14 from "path";
9766
+ import * as path15 from "path";
9283
9767
  import matter3 from "gray-matter";
9284
9768
  var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
9285
9769
  var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
9286
9770
  var WIKILINK_REGEX2 = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]+)?\]\]/g;
9287
9771
  async function readFileContent2(notePath, vaultPath2) {
9288
- const fullPath = path14.join(vaultPath2, notePath);
9772
+ const fullPath = path15.join(vaultPath2, notePath);
9289
9773
  try {
9290
9774
  return await fs15.readFile(fullPath, "utf-8");
9291
9775
  } catch {
@@ -9555,10 +10039,10 @@ async function validateCrossLayer(index, notePath, vaultPath2) {
9555
10039
 
9556
10040
  // src/tools/read/computed.ts
9557
10041
  import * as fs16 from "fs/promises";
9558
- import * as path15 from "path";
10042
+ import * as path16 from "path";
9559
10043
  import matter4 from "gray-matter";
9560
10044
  async function readFileContent3(notePath, vaultPath2) {
9561
- const fullPath = path15.join(vaultPath2, notePath);
10045
+ const fullPath = path16.join(vaultPath2, notePath);
9562
10046
  try {
9563
10047
  return await fs16.readFile(fullPath, "utf-8");
9564
10048
  } catch {
@@ -9566,7 +10050,7 @@ async function readFileContent3(notePath, vaultPath2) {
9566
10050
  }
9567
10051
  }
9568
10052
  async function getFileStats(notePath, vaultPath2) {
9569
- const fullPath = path15.join(vaultPath2, notePath);
10053
+ const fullPath = path16.join(vaultPath2, notePath);
9570
10054
  try {
9571
10055
  const stats = await fs16.stat(fullPath);
9572
10056
  return {
@@ -9697,12 +10181,14 @@ async function computeFrontmatter(index, notePath, vaultPath2, fields) {
9697
10181
  }
9698
10182
 
9699
10183
  // src/tools/read/noteIntelligence.ts
10184
+ import fs17 from "node:fs";
10185
+ import nodePath from "node:path";
9700
10186
  function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
9701
10187
  server2.registerTool(
9702
10188
  "note_intelligence",
9703
10189
  {
9704
10190
  title: "Note Intelligence",
9705
- description: 'Analyze a note for patterns, suggestions, and consistency. Use analysis to pick the mode:\n- "prose_patterns": Find "Key: Value" or "Key: [[wikilink]]" patterns in prose\n- "suggest_frontmatter": Suggest YAML frontmatter from detected prose patterns\n- "suggest_wikilinks": Find frontmatter values that could be wikilinks\n- "cross_layer": Check consistency between frontmatter and prose references\n- "compute": Auto-compute derived fields (word_count, link_count, etc.)\n- "all": Run all analyses and return combined result\n\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "wikilinks" })\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "compute", fields: ["word_count", "link_count"] })',
10191
+ description: 'Analyze a note for patterns, suggestions, and consistency. Use analysis to pick the mode:\n- "prose_patterns": Find "Key: Value" or "Key: [[wikilink]]" patterns in prose\n- "suggest_frontmatter": Suggest YAML frontmatter from detected prose patterns\n- "suggest_wikilinks": Find frontmatter values that could be wikilinks\n- "cross_layer": Check consistency between frontmatter and prose references\n- "compute": Auto-compute derived fields (word_count, link_count, etc.)\n- "semantic_links": Find semantically related entities not currently linked in the note (requires init_semantic)\n- "all": Run all analyses and return combined result\n\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "wikilinks" })\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "compute", fields: ["word_count", "link_count"] })\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "semantic_links" })',
9706
10192
  inputSchema: {
9707
10193
  analysis: z10.enum([
9708
10194
  "prose_patterns",
@@ -9710,6 +10196,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
9710
10196
  "suggest_wikilinks",
9711
10197
  "cross_layer",
9712
10198
  "compute",
10199
+ "semantic_links",
9713
10200
  "all"
9714
10201
  ]).describe("Type of note analysis to perform"),
9715
10202
  path: z10.string().describe("Path to the note to analyze"),
@@ -9751,6 +10238,54 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
9751
10238
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
9752
10239
  };
9753
10240
  }
10241
+ case "semantic_links": {
10242
+ if (!hasEntityEmbeddingsIndex()) {
10243
+ return {
10244
+ content: [{ type: "text", text: JSON.stringify({
10245
+ error: "Entity embeddings not available. Run init_semantic first."
10246
+ }, null, 2) }]
10247
+ };
10248
+ }
10249
+ let noteContent;
10250
+ try {
10251
+ noteContent = fs17.readFileSync(nodePath.join(vaultPath2, notePath), "utf-8");
10252
+ } catch {
10253
+ return {
10254
+ content: [{ type: "text", text: JSON.stringify({
10255
+ error: `Could not read note: ${notePath}`
10256
+ }, null, 2) }]
10257
+ };
10258
+ }
10259
+ const linkedEntities = /* @__PURE__ */ new Set();
10260
+ const wikilinkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
10261
+ let wlMatch;
10262
+ while ((wlMatch = wikilinkRegex.exec(noteContent)) !== null) {
10263
+ linkedEntities.add(wlMatch[1].toLowerCase());
10264
+ }
10265
+ try {
10266
+ const contentEmbedding = await embedTextCached(noteContent);
10267
+ const matches = findSemanticallySimilarEntities(contentEmbedding, 20, linkedEntities);
10268
+ const suggestions = matches.filter((m) => m.similarity >= 0.3).map((m) => ({
10269
+ entity: m.entityName,
10270
+ similarity: m.similarity
10271
+ }));
10272
+ return {
10273
+ content: [{ type: "text", text: JSON.stringify({
10274
+ path: notePath,
10275
+ analysis: "semantic_links",
10276
+ linked_count: linkedEntities.size,
10277
+ suggestion_count: suggestions.length,
10278
+ suggestions
10279
+ }, null, 2) }]
10280
+ };
10281
+ } catch {
10282
+ return {
10283
+ content: [{ type: "text", text: JSON.stringify({
10284
+ error: "Failed to compute semantic links"
10285
+ }, null, 2) }]
10286
+ };
10287
+ }
10288
+ }
9754
10289
  case "all": {
9755
10290
  const [prosePatterns, suggestedFrontmatter, suggestedWikilinks, crossLayer, computed] = await Promise.all([
9756
10291
  detectProsePatterns(index, notePath, vaultPath2),
@@ -9778,8 +10313,8 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
9778
10313
  // src/tools/write/mutations.ts
9779
10314
  init_writer();
9780
10315
  import { z as z11 } from "zod";
9781
- import fs19 from "fs/promises";
9782
- import path18 from "path";
10316
+ import fs20 from "fs/promises";
10317
+ import path19 from "path";
9783
10318
 
9784
10319
  // src/core/write/validator.ts
9785
10320
  var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
@@ -9981,8 +10516,8 @@ function runValidationPipeline(content, format, options = {}) {
9981
10516
 
9982
10517
  // src/core/write/mutation-helpers.ts
9983
10518
  init_writer();
9984
- import fs18 from "fs/promises";
9985
- import path17 from "path";
10519
+ import fs19 from "fs/promises";
10520
+ import path18 from "path";
9986
10521
  init_constants();
9987
10522
  init_writer();
9988
10523
  function formatMcpResult(result) {
@@ -10031,9 +10566,9 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
10031
10566
  return info;
10032
10567
  }
10033
10568
  async function ensureFileExists(vaultPath2, notePath) {
10034
- const fullPath = path17.join(vaultPath2, notePath);
10569
+ const fullPath = path18.join(vaultPath2, notePath);
10035
10570
  try {
10036
- await fs18.access(fullPath);
10571
+ await fs19.access(fullPath);
10037
10572
  return null;
10038
10573
  } catch {
10039
10574
  return errorResult(notePath, `File not found: ${notePath}`);
@@ -10097,9 +10632,14 @@ async function withVaultFile(options, operation) {
10097
10632
  });
10098
10633
  return formatMcpResult(result);
10099
10634
  } catch (error) {
10635
+ const extras = {};
10636
+ if (error instanceof DiagnosticError) {
10637
+ extras.diagnostic = error.diagnostic;
10638
+ }
10100
10639
  const result = errorResult(
10101
10640
  notePath,
10102
- `Failed to ${actionDescription}: ${error instanceof Error ? error.message : String(error)}`
10641
+ `Failed to ${actionDescription}: ${error instanceof Error ? error.message : String(error)}`,
10642
+ extras
10103
10643
  );
10104
10644
  return formatMcpResult(result);
10105
10645
  }
@@ -10131,10 +10671,10 @@ async function withVaultFrontmatter(options, operation) {
10131
10671
 
10132
10672
  // src/tools/write/mutations.ts
10133
10673
  async function createNoteFromTemplate(vaultPath2, notePath, config) {
10134
- const fullPath = path18.join(vaultPath2, notePath);
10135
- await fs19.mkdir(path18.dirname(fullPath), { recursive: true });
10674
+ const fullPath = path19.join(vaultPath2, notePath);
10675
+ await fs20.mkdir(path19.dirname(fullPath), { recursive: true });
10136
10676
  const templates = config.templates || {};
10137
- const filename = path18.basename(notePath, ".md").toLowerCase();
10677
+ const filename = path19.basename(notePath, ".md").toLowerCase();
10138
10678
  let templatePath;
10139
10679
  const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
10140
10680
  const weeklyPattern = /^\d{4}-W\d{2}/;
@@ -10155,10 +10695,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10155
10695
  let templateContent;
10156
10696
  if (templatePath) {
10157
10697
  try {
10158
- const absTemplatePath = path18.join(vaultPath2, templatePath);
10159
- templateContent = await fs19.readFile(absTemplatePath, "utf-8");
10698
+ const absTemplatePath = path19.join(vaultPath2, templatePath);
10699
+ templateContent = await fs20.readFile(absTemplatePath, "utf-8");
10160
10700
  } catch {
10161
- const title = path18.basename(notePath, ".md");
10701
+ const title = path19.basename(notePath, ".md");
10162
10702
  templateContent = `---
10163
10703
  ---
10164
10704
 
@@ -10167,7 +10707,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10167
10707
  templatePath = void 0;
10168
10708
  }
10169
10709
  } else {
10170
- const title = path18.basename(notePath, ".md");
10710
+ const title = path19.basename(notePath, ".md");
10171
10711
  templateContent = `---
10172
10712
  ---
10173
10713
 
@@ -10176,8 +10716,8 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10176
10716
  }
10177
10717
  const now = /* @__PURE__ */ new Date();
10178
10718
  const dateStr = now.toISOString().split("T")[0];
10179
- templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path18.basename(notePath, ".md"));
10180
- await fs19.writeFile(fullPath, templateContent, "utf-8");
10719
+ templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path19.basename(notePath, ".md"));
10720
+ await fs20.writeFile(fullPath, templateContent, "utf-8");
10181
10721
  return { created: true, templateUsed: templatePath };
10182
10722
  }
10183
10723
  function registerMutationTools(server2, vaultPath2, getConfig = () => ({})) {
@@ -10196,6 +10736,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
10196
10736
  commit: z11.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
10197
10737
  skipWikilinks: z11.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
10198
10738
  preserveListNesting: z11.boolean().default(true).describe("Detect and preserve the indentation level of surrounding list items. Set false to disable."),
10739
+ bumpHeadings: z11.boolean().default(true).describe("Auto-bump heading levels in inserted content so they nest under the target section (e.g., ## in a ## section becomes ###). Set false to disable."),
10199
10740
  suggestOutgoingLinks: z11.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]], [[Philosophy]]"). Set false to disable.'),
10200
10741
  maxSuggestions: z11.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)"),
10201
10742
  validate: z11.boolean().default(true).describe("Check input for common issues (double timestamps, non-markdown bullets, etc.)"),
@@ -10204,13 +10745,13 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
10204
10745
  agent_id: z11.string().optional().describe('Agent identifier for multi-agent scoping (e.g., "claude-opus", "planning-agent")'),
10205
10746
  session_id: z11.string().optional().describe('Session identifier for conversation scoping (e.g., "sess-abc123")')
10206
10747
  },
10207
- async ({ path: notePath, section, content, create_if_missing, position, format, commit, skipWikilinks, preserveListNesting, suggestOutgoingLinks, maxSuggestions, validate, normalize, guardrails, agent_id, session_id }) => {
10748
+ async ({ path: notePath, section, content, create_if_missing, position, format, commit, skipWikilinks, preserveListNesting, bumpHeadings, suggestOutgoingLinks, maxSuggestions, validate, normalize, guardrails, agent_id, session_id }) => {
10208
10749
  let noteCreated = false;
10209
10750
  let templateUsed;
10210
10751
  if (create_if_missing) {
10211
- const fullPath = path18.join(vaultPath2, notePath);
10752
+ const fullPath = path19.join(vaultPath2, notePath);
10212
10753
  try {
10213
- await fs19.access(fullPath);
10754
+ await fs20.access(fullPath);
10214
10755
  } catch {
10215
10756
  const config = getConfig();
10216
10757
  const result = await createNoteFromTemplate(vaultPath2, notePath, config);
@@ -10247,7 +10788,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
10247
10788
  };
10248
10789
  let suggestInfo;
10249
10790
  if (suggestOutgoingLinks && !skipWikilinks) {
10250
- const result = suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
10791
+ const result = await suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
10251
10792
  if (result.suffix) {
10252
10793
  processedContent = processedContent + " " + result.suffix;
10253
10794
  suggestInfo = `Suggested: ${result.suggestions.join(", ")}`;
@@ -10259,7 +10800,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
10259
10800
  ctx.sectionBoundary,
10260
10801
  formattedContent,
10261
10802
  position,
10262
- { preserveListNesting }
10803
+ { preserveListNesting, bumpHeadings }
10263
10804
  );
10264
10805
  const infoLines = [wikilinkInfo, suggestInfo].filter(Boolean);
10265
10806
  const preview = formattedContent + (infoLines.length > 0 ? `
@@ -10366,7 +10907,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
10366
10907
  let workingReplacement = validationResult.content;
10367
10908
  let { content: processedReplacement } = maybeApplyWikilinks(workingReplacement, skipWikilinks, notePath);
10368
10909
  if (suggestOutgoingLinks && !skipWikilinks) {
10369
- const result = suggestRelatedLinks(processedReplacement, { maxSuggestions, notePath });
10910
+ const result = await suggestRelatedLinks(processedReplacement, { maxSuggestions, notePath });
10370
10911
  if (result.suffix) {
10371
10912
  processedReplacement = processedReplacement + " " + result.suffix;
10372
10913
  }
@@ -10380,7 +10921,20 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
10380
10921
  useRegex
10381
10922
  );
10382
10923
  if (replaceResult.replacedCount === 0) {
10383
- throw new Error(`No content matching "${search}" found in section "${ctx.sectionBoundary.name}"`);
10924
+ const lines = ctx.content.split("\n");
10925
+ const boundary = ctx.sectionBoundary;
10926
+ const sectionLines = lines.slice(boundary.contentStartLine, boundary.endLine + 1);
10927
+ const sectionContent = sectionLines.join("\n");
10928
+ const diagnostic = buildReplaceNotFoundDiagnostic(
10929
+ sectionContent,
10930
+ search,
10931
+ boundary.name,
10932
+ boundary.contentStartLine
10933
+ );
10934
+ throw new DiagnosticError(
10935
+ `No content matching "${search}" found in section "${boundary.name}"`,
10936
+ diagnostic
10937
+ );
10384
10938
  }
10385
10939
  const previewLines = replaceResult.originalLines.map(
10386
10940
  (orig, i) => `- ${orig}
@@ -10551,7 +11105,7 @@ function registerTaskTools(server2, vaultPath2) {
10551
11105
  let { content: processedTask, wikilinkInfo } = maybeApplyWikilinks(workingTask, skipWikilinks, notePath);
10552
11106
  let suggestInfo;
10553
11107
  if (suggestOutgoingLinks && !skipWikilinks) {
10554
- const result = suggestRelatedLinks(processedTask, { maxSuggestions, notePath });
11108
+ const result = await suggestRelatedLinks(processedTask, { maxSuggestions, notePath });
10555
11109
  if (result.suffix) {
10556
11110
  processedTask = processedTask + " " + result.suffix;
10557
11111
  suggestInfo = `Suggested: ${result.suggestions.join(", ")}`;
@@ -10645,8 +11199,8 @@ Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { st
10645
11199
  // src/tools/write/notes.ts
10646
11200
  init_writer();
10647
11201
  import { z as z14 } from "zod";
10648
- import fs20 from "fs/promises";
10649
- import path19 from "path";
11202
+ import fs21 from "fs/promises";
11203
+ import path20 from "path";
10650
11204
  function registerNoteTools(server2, vaultPath2, getIndex) {
10651
11205
  server2.tool(
10652
11206
  "vault_create_note",
@@ -10669,23 +11223,23 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
10669
11223
  if (!validatePath(vaultPath2, notePath)) {
10670
11224
  return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
10671
11225
  }
10672
- const fullPath = path19.join(vaultPath2, notePath);
11226
+ const fullPath = path20.join(vaultPath2, notePath);
10673
11227
  const existsCheck = await ensureFileExists(vaultPath2, notePath);
10674
11228
  if (existsCheck === null && !overwrite) {
10675
11229
  return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
10676
11230
  }
10677
- const dir = path19.dirname(fullPath);
10678
- await fs20.mkdir(dir, { recursive: true });
11231
+ const dir = path20.dirname(fullPath);
11232
+ await fs21.mkdir(dir, { recursive: true });
10679
11233
  let effectiveContent = content;
10680
11234
  let effectiveFrontmatter = frontmatter;
10681
11235
  if (template) {
10682
- const templatePath = path19.join(vaultPath2, template);
11236
+ const templatePath = path20.join(vaultPath2, template);
10683
11237
  try {
10684
- const raw = await fs20.readFile(templatePath, "utf-8");
11238
+ const raw = await fs21.readFile(templatePath, "utf-8");
10685
11239
  const matter9 = (await import("gray-matter")).default;
10686
11240
  const parsed = matter9(raw);
10687
11241
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
10688
- const title = path19.basename(notePath, ".md");
11242
+ const title = path20.basename(notePath, ".md");
10689
11243
  let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
10690
11244
  if (content) {
10691
11245
  templateContent = templateContent.trimEnd() + "\n\n" + content;
@@ -10697,9 +11251,9 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
10697
11251
  }
10698
11252
  }
10699
11253
  const warnings = [];
10700
- const noteName = path19.basename(notePath, ".md");
11254
+ const noteName = path20.basename(notePath, ".md");
10701
11255
  const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
10702
- const preflight = checkPreflightSimilarity(noteName);
11256
+ const preflight = await checkPreflightSimilarity(noteName);
10703
11257
  if (preflight.existingEntity) {
10704
11258
  warnings.push({
10705
11259
  type: "similar_note_exists",
@@ -10725,7 +11279,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
10725
11279
  let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(effectiveContent, skipWikilinks, notePath);
10726
11280
  let suggestInfo;
10727
11281
  if (suggestOutgoingLinks && !skipWikilinks) {
10728
- const result = suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
11282
+ const result = await suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
10729
11283
  if (result.suffix) {
10730
11284
  processedContent = processedContent + " " + result.suffix;
10731
11285
  suggestInfo = `Suggested: ${result.suggestions.join(", ")}`;
@@ -10814,8 +11368,8 @@ ${sources}`;
10814
11368
  }
10815
11369
  return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
10816
11370
  }
10817
- const fullPath = path19.join(vaultPath2, notePath);
10818
- await fs20.unlink(fullPath);
11371
+ const fullPath = path20.join(vaultPath2, notePath);
11372
+ await fs21.unlink(fullPath);
10819
11373
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
10820
11374
  const message = backlinkWarning ? `Deleted note: ${notePath}
10821
11375
 
@@ -10833,8 +11387,8 @@ Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
10833
11387
  // src/tools/write/move-notes.ts
10834
11388
  init_writer();
10835
11389
  import { z as z15 } from "zod";
10836
- import fs21 from "fs/promises";
10837
- import path20 from "path";
11390
+ import fs22 from "fs/promises";
11391
+ import path21 from "path";
10838
11392
  import matter6 from "gray-matter";
10839
11393
  function escapeRegex(str) {
10840
11394
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -10853,16 +11407,16 @@ function extractWikilinks2(content) {
10853
11407
  return wikilinks;
10854
11408
  }
10855
11409
  function getTitleFromPath(filePath) {
10856
- return path20.basename(filePath, ".md");
11410
+ return path21.basename(filePath, ".md");
10857
11411
  }
10858
11412
  async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
10859
11413
  const results = [];
10860
11414
  const allTargets = [targetTitle, ...targetAliases].map((t) => t.toLowerCase());
10861
11415
  async function scanDir(dir) {
10862
11416
  const files = [];
10863
- const entries = await fs21.readdir(dir, { withFileTypes: true });
11417
+ const entries = await fs22.readdir(dir, { withFileTypes: true });
10864
11418
  for (const entry of entries) {
10865
- const fullPath = path20.join(dir, entry.name);
11419
+ const fullPath = path21.join(dir, entry.name);
10866
11420
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
10867
11421
  files.push(...await scanDir(fullPath));
10868
11422
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -10873,8 +11427,8 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
10873
11427
  }
10874
11428
  const allFiles = await scanDir(vaultPath2);
10875
11429
  for (const filePath of allFiles) {
10876
- const relativePath = path20.relative(vaultPath2, filePath);
10877
- const content = await fs21.readFile(filePath, "utf-8");
11430
+ const relativePath = path21.relative(vaultPath2, filePath);
11431
+ const content = await fs22.readFile(filePath, "utf-8");
10878
11432
  const wikilinks = extractWikilinks2(content);
10879
11433
  const matchingLinks = [];
10880
11434
  for (const link of wikilinks) {
@@ -10893,8 +11447,8 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
10893
11447
  return results;
10894
11448
  }
10895
11449
  async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
10896
- const fullPath = path20.join(vaultPath2, filePath);
10897
- const raw = await fs21.readFile(fullPath, "utf-8");
11450
+ const fullPath = path21.join(vaultPath2, filePath);
11451
+ const raw = await fs22.readFile(fullPath, "utf-8");
10898
11452
  const parsed = matter6(raw);
10899
11453
  let content = parsed.content;
10900
11454
  let totalUpdated = 0;
@@ -10960,10 +11514,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
10960
11514
  };
10961
11515
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
10962
11516
  }
10963
- const oldFullPath = path20.join(vaultPath2, oldPath);
10964
- const newFullPath = path20.join(vaultPath2, newPath);
11517
+ const oldFullPath = path21.join(vaultPath2, oldPath);
11518
+ const newFullPath = path21.join(vaultPath2, newPath);
10965
11519
  try {
10966
- await fs21.access(oldFullPath);
11520
+ await fs22.access(oldFullPath);
10967
11521
  } catch {
10968
11522
  const result2 = {
10969
11523
  success: false,
@@ -10973,7 +11527,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
10973
11527
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
10974
11528
  }
10975
11529
  try {
10976
- await fs21.access(newFullPath);
11530
+ await fs22.access(newFullPath);
10977
11531
  const result2 = {
10978
11532
  success: false,
10979
11533
  message: `Destination already exists: ${newPath}`,
@@ -10982,7 +11536,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
10982
11536
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
10983
11537
  } catch {
10984
11538
  }
10985
- const sourceContent = await fs21.readFile(oldFullPath, "utf-8");
11539
+ const sourceContent = await fs22.readFile(oldFullPath, "utf-8");
10986
11540
  const parsed = matter6(sourceContent);
10987
11541
  const aliases = extractAliases2(parsed.data);
10988
11542
  const oldTitle = getTitleFromPath(oldPath);
@@ -11011,9 +11565,9 @@ function registerMoveNoteTools(server2, vaultPath2) {
11011
11565
  }
11012
11566
  }
11013
11567
  }
11014
- const destDir = path20.dirname(newFullPath);
11015
- await fs21.mkdir(destDir, { recursive: true });
11016
- await fs21.rename(oldFullPath, newFullPath);
11568
+ const destDir = path21.dirname(newFullPath);
11569
+ await fs22.mkdir(destDir, { recursive: true });
11570
+ await fs22.rename(oldFullPath, newFullPath);
11017
11571
  let gitCommit;
11018
11572
  let undoAvailable;
11019
11573
  let staleLockDetected;
@@ -11097,12 +11651,12 @@ function registerMoveNoteTools(server2, vaultPath2) {
11097
11651
  if (sanitizedTitle !== newTitle) {
11098
11652
  console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
11099
11653
  }
11100
- const fullPath = path20.join(vaultPath2, notePath);
11101
- const dir = path20.dirname(notePath);
11102
- const newPath = dir === "." ? `${sanitizedTitle}.md` : path20.join(dir, `${sanitizedTitle}.md`);
11103
- const newFullPath = path20.join(vaultPath2, newPath);
11654
+ const fullPath = path21.join(vaultPath2, notePath);
11655
+ const dir = path21.dirname(notePath);
11656
+ const newPath = dir === "." ? `${sanitizedTitle}.md` : path21.join(dir, `${sanitizedTitle}.md`);
11657
+ const newFullPath = path21.join(vaultPath2, newPath);
11104
11658
  try {
11105
- await fs21.access(fullPath);
11659
+ await fs22.access(fullPath);
11106
11660
  } catch {
11107
11661
  const result2 = {
11108
11662
  success: false,
@@ -11113,7 +11667,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
11113
11667
  }
11114
11668
  if (fullPath !== newFullPath) {
11115
11669
  try {
11116
- await fs21.access(newFullPath);
11670
+ await fs22.access(newFullPath);
11117
11671
  const result2 = {
11118
11672
  success: false,
11119
11673
  message: `A note with this title already exists: ${newPath}`,
@@ -11123,7 +11677,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
11123
11677
  } catch {
11124
11678
  }
11125
11679
  }
11126
- const sourceContent = await fs21.readFile(fullPath, "utf-8");
11680
+ const sourceContent = await fs22.readFile(fullPath, "utf-8");
11127
11681
  const parsed = matter6(sourceContent);
11128
11682
  const aliases = extractAliases2(parsed.data);
11129
11683
  const oldTitle = getTitleFromPath(notePath);
@@ -11152,7 +11706,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
11152
11706
  }
11153
11707
  }
11154
11708
  if (fullPath !== newFullPath) {
11155
- await fs21.rename(fullPath, newFullPath);
11709
+ await fs22.rename(fullPath, newFullPath);
11156
11710
  }
11157
11711
  let gitCommit;
11158
11712
  let undoAvailable;
@@ -11325,8 +11879,8 @@ init_schema();
11325
11879
 
11326
11880
  // src/core/write/policy/parser.ts
11327
11881
  init_schema();
11328
- import fs22 from "fs/promises";
11329
- import path21 from "path";
11882
+ import fs23 from "fs/promises";
11883
+ import path22 from "path";
11330
11884
  import matter7 from "gray-matter";
11331
11885
  function parseYaml(content) {
11332
11886
  const parsed = matter7(`---
@@ -11351,7 +11905,7 @@ function parsePolicyString(yamlContent) {
11351
11905
  }
11352
11906
  async function loadPolicyFile(filePath) {
11353
11907
  try {
11354
- const content = await fs22.readFile(filePath, "utf-8");
11908
+ const content = await fs23.readFile(filePath, "utf-8");
11355
11909
  return parsePolicyString(content);
11356
11910
  } catch (error) {
11357
11911
  if (error.code === "ENOENT") {
@@ -11375,15 +11929,15 @@ async function loadPolicyFile(filePath) {
11375
11929
  }
11376
11930
  }
11377
11931
  async function loadPolicy(vaultPath2, policyName) {
11378
- const policiesDir = path21.join(vaultPath2, ".claude", "policies");
11379
- const policyPath = path21.join(policiesDir, `${policyName}.yaml`);
11932
+ const policiesDir = path22.join(vaultPath2, ".claude", "policies");
11933
+ const policyPath = path22.join(policiesDir, `${policyName}.yaml`);
11380
11934
  try {
11381
- await fs22.access(policyPath);
11935
+ await fs23.access(policyPath);
11382
11936
  return loadPolicyFile(policyPath);
11383
11937
  } catch {
11384
- const ymlPath = path21.join(policiesDir, `${policyName}.yml`);
11938
+ const ymlPath = path22.join(policiesDir, `${policyName}.yml`);
11385
11939
  try {
11386
- await fs22.access(ymlPath);
11940
+ await fs23.access(ymlPath);
11387
11941
  return loadPolicyFile(ymlPath);
11388
11942
  } catch {
11389
11943
  return {
@@ -11521,8 +12075,8 @@ init_template();
11521
12075
  init_conditions();
11522
12076
  init_schema();
11523
12077
  init_writer();
11524
- import fs24 from "fs/promises";
11525
- import path23 from "path";
12078
+ import fs25 from "fs/promises";
12079
+ import path24 from "path";
11526
12080
  init_constants();
11527
12081
  async function executeStep(step, vaultPath2, context, conditionResults) {
11528
12082
  const { execute, reason } = shouldStepExecute(step.when, conditionResults);
@@ -11591,9 +12145,9 @@ async function executeAddToSection(params, vaultPath2, context) {
11591
12145
  const preserveListNesting = params.preserveListNesting !== false;
11592
12146
  const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
11593
12147
  const maxSuggestions = Number(params.maxSuggestions) || 3;
11594
- const fullPath = path23.join(vaultPath2, notePath);
12148
+ const fullPath = path24.join(vaultPath2, notePath);
11595
12149
  try {
11596
- await fs24.access(fullPath);
12150
+ await fs25.access(fullPath);
11597
12151
  } catch {
11598
12152
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
11599
12153
  }
@@ -11631,9 +12185,9 @@ async function executeRemoveFromSection(params, vaultPath2) {
11631
12185
  const pattern = String(params.pattern || "");
11632
12186
  const mode = params.mode || "first";
11633
12187
  const useRegex = Boolean(params.useRegex);
11634
- const fullPath = path23.join(vaultPath2, notePath);
12188
+ const fullPath = path24.join(vaultPath2, notePath);
11635
12189
  try {
11636
- await fs24.access(fullPath);
12190
+ await fs25.access(fullPath);
11637
12191
  } catch {
11638
12192
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
11639
12193
  }
@@ -11662,9 +12216,9 @@ async function executeReplaceInSection(params, vaultPath2, context) {
11662
12216
  const mode = params.mode || "first";
11663
12217
  const useRegex = Boolean(params.useRegex);
11664
12218
  const skipWikilinks = Boolean(params.skipWikilinks);
11665
- const fullPath = path23.join(vaultPath2, notePath);
12219
+ const fullPath = path24.join(vaultPath2, notePath);
11666
12220
  try {
11667
- await fs24.access(fullPath);
12221
+ await fs25.access(fullPath);
11668
12222
  } catch {
11669
12223
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
11670
12224
  }
@@ -11705,16 +12259,16 @@ async function executeCreateNote(params, vaultPath2, context) {
11705
12259
  if (!validatePath(vaultPath2, notePath)) {
11706
12260
  return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
11707
12261
  }
11708
- const fullPath = path23.join(vaultPath2, notePath);
12262
+ const fullPath = path24.join(vaultPath2, notePath);
11709
12263
  try {
11710
- await fs24.access(fullPath);
12264
+ await fs25.access(fullPath);
11711
12265
  if (!overwrite) {
11712
12266
  return { success: false, message: `File already exists: ${notePath}`, path: notePath };
11713
12267
  }
11714
12268
  } catch {
11715
12269
  }
11716
- const dir = path23.dirname(fullPath);
11717
- await fs24.mkdir(dir, { recursive: true });
12270
+ const dir = path24.dirname(fullPath);
12271
+ await fs25.mkdir(dir, { recursive: true });
11718
12272
  const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
11719
12273
  await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
11720
12274
  return {
@@ -11733,13 +12287,13 @@ async function executeDeleteNote(params, vaultPath2) {
11733
12287
  if (!validatePath(vaultPath2, notePath)) {
11734
12288
  return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
11735
12289
  }
11736
- const fullPath = path23.join(vaultPath2, notePath);
12290
+ const fullPath = path24.join(vaultPath2, notePath);
11737
12291
  try {
11738
- await fs24.access(fullPath);
12292
+ await fs25.access(fullPath);
11739
12293
  } catch {
11740
12294
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
11741
12295
  }
11742
- await fs24.unlink(fullPath);
12296
+ await fs25.unlink(fullPath);
11743
12297
  return {
11744
12298
  success: true,
11745
12299
  message: `Deleted note: ${notePath}`,
@@ -11750,9 +12304,9 @@ async function executeToggleTask(params, vaultPath2) {
11750
12304
  const notePath = String(params.path || "");
11751
12305
  const task = String(params.task || "");
11752
12306
  const section = params.section ? String(params.section) : void 0;
11753
- const fullPath = path23.join(vaultPath2, notePath);
12307
+ const fullPath = path24.join(vaultPath2, notePath);
11754
12308
  try {
11755
- await fs24.access(fullPath);
12309
+ await fs25.access(fullPath);
11756
12310
  } catch {
11757
12311
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
11758
12312
  }
@@ -11793,9 +12347,9 @@ async function executeAddTask(params, vaultPath2, context) {
11793
12347
  const completed = Boolean(params.completed);
11794
12348
  const skipWikilinks = Boolean(params.skipWikilinks);
11795
12349
  const preserveListNesting = params.preserveListNesting !== false;
11796
- const fullPath = path23.join(vaultPath2, notePath);
12350
+ const fullPath = path24.join(vaultPath2, notePath);
11797
12351
  try {
11798
- await fs24.access(fullPath);
12352
+ await fs25.access(fullPath);
11799
12353
  } catch {
11800
12354
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
11801
12355
  }
@@ -11830,9 +12384,9 @@ async function executeAddTask(params, vaultPath2, context) {
11830
12384
  async function executeUpdateFrontmatter(params, vaultPath2) {
11831
12385
  const notePath = String(params.path || "");
11832
12386
  const updates = params.frontmatter || {};
11833
- const fullPath = path23.join(vaultPath2, notePath);
12387
+ const fullPath = path24.join(vaultPath2, notePath);
11834
12388
  try {
11835
- await fs24.access(fullPath);
12389
+ await fs25.access(fullPath);
11836
12390
  } catch {
11837
12391
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
11838
12392
  }
@@ -11852,9 +12406,9 @@ async function executeAddFrontmatterField(params, vaultPath2) {
11852
12406
  const notePath = String(params.path || "");
11853
12407
  const key = String(params.key || "");
11854
12408
  const value = params.value;
11855
- const fullPath = path23.join(vaultPath2, notePath);
12409
+ const fullPath = path24.join(vaultPath2, notePath);
11856
12410
  try {
11857
- await fs24.access(fullPath);
12411
+ await fs25.access(fullPath);
11858
12412
  } catch {
11859
12413
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
11860
12414
  }
@@ -12012,15 +12566,15 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
12012
12566
  async function rollbackChanges(vaultPath2, originalContents, filesModified) {
12013
12567
  for (const filePath of filesModified) {
12014
12568
  const original = originalContents.get(filePath);
12015
- const fullPath = path23.join(vaultPath2, filePath);
12569
+ const fullPath = path24.join(vaultPath2, filePath);
12016
12570
  if (original === null) {
12017
12571
  try {
12018
- await fs24.unlink(fullPath);
12572
+ await fs25.unlink(fullPath);
12019
12573
  } catch {
12020
12574
  }
12021
12575
  } else if (original !== void 0) {
12022
12576
  try {
12023
- await fs24.writeFile(fullPath, original);
12577
+ await fs25.writeFile(fullPath, original);
12024
12578
  } catch {
12025
12579
  }
12026
12580
  }
@@ -12066,27 +12620,27 @@ async function previewPolicy(policy, vaultPath2, variables) {
12066
12620
  }
12067
12621
 
12068
12622
  // src/core/write/policy/storage.ts
12069
- import fs25 from "fs/promises";
12070
- import path24 from "path";
12623
+ import fs26 from "fs/promises";
12624
+ import path25 from "path";
12071
12625
  function getPoliciesDir(vaultPath2) {
12072
- return path24.join(vaultPath2, ".claude", "policies");
12626
+ return path25.join(vaultPath2, ".claude", "policies");
12073
12627
  }
12074
12628
  async function ensurePoliciesDir(vaultPath2) {
12075
12629
  const dir = getPoliciesDir(vaultPath2);
12076
- await fs25.mkdir(dir, { recursive: true });
12630
+ await fs26.mkdir(dir, { recursive: true });
12077
12631
  }
12078
12632
  async function listPolicies(vaultPath2) {
12079
12633
  const dir = getPoliciesDir(vaultPath2);
12080
12634
  const policies = [];
12081
12635
  try {
12082
- const files = await fs25.readdir(dir);
12636
+ const files = await fs26.readdir(dir);
12083
12637
  for (const file of files) {
12084
12638
  if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
12085
12639
  continue;
12086
12640
  }
12087
- const filePath = path24.join(dir, file);
12088
- const stat3 = await fs25.stat(filePath);
12089
- const content = await fs25.readFile(filePath, "utf-8");
12641
+ const filePath = path25.join(dir, file);
12642
+ const stat3 = await fs26.stat(filePath);
12643
+ const content = await fs26.readFile(filePath, "utf-8");
12090
12644
  const metadata = extractPolicyMetadata(content);
12091
12645
  policies.push({
12092
12646
  name: metadata.name || file.replace(/\.ya?ml$/, ""),
@@ -12109,10 +12663,10 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
12109
12663
  const dir = getPoliciesDir(vaultPath2);
12110
12664
  await ensurePoliciesDir(vaultPath2);
12111
12665
  const filename = `${policyName}.yaml`;
12112
- const filePath = path24.join(dir, filename);
12666
+ const filePath = path25.join(dir, filename);
12113
12667
  if (!overwrite) {
12114
12668
  try {
12115
- await fs25.access(filePath);
12669
+ await fs26.access(filePath);
12116
12670
  return {
12117
12671
  success: false,
12118
12672
  path: filename,
@@ -12129,7 +12683,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
12129
12683
  message: `Invalid policy: ${validation.errors.map((e) => e.message).join("; ")}`
12130
12684
  };
12131
12685
  }
12132
- await fs25.writeFile(filePath, content, "utf-8");
12686
+ await fs26.writeFile(filePath, content, "utf-8");
12133
12687
  return {
12134
12688
  success: true,
12135
12689
  path: filename,
@@ -12652,8 +13206,8 @@ function registerPolicyTools(server2, vaultPath2) {
12652
13206
  import { z as z19 } from "zod";
12653
13207
 
12654
13208
  // src/core/write/tagRename.ts
12655
- import * as fs26 from "fs/promises";
12656
- import * as path25 from "path";
13209
+ import * as fs27 from "fs/promises";
13210
+ import * as path26 from "path";
12657
13211
  import matter8 from "gray-matter";
12658
13212
  import { getProtectedZones } from "@velvetmonkey/vault-core";
12659
13213
  function getNotesInFolder3(index, folder) {
@@ -12759,10 +13313,10 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
12759
13313
  const previews = [];
12760
13314
  let totalChanges = 0;
12761
13315
  for (const note of affectedNotes) {
12762
- const fullPath = path25.join(vaultPath2, note.path);
13316
+ const fullPath = path26.join(vaultPath2, note.path);
12763
13317
  let fileContent;
12764
13318
  try {
12765
- fileContent = await fs26.readFile(fullPath, "utf-8");
13319
+ fileContent = await fs27.readFile(fullPath, "utf-8");
12766
13320
  } catch {
12767
13321
  continue;
12768
13322
  }
@@ -12835,7 +13389,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
12835
13389
  previews.push(preview);
12836
13390
  if (!dryRun) {
12837
13391
  const newContent = matter8.stringify(updatedContent, fm);
12838
- await fs26.writeFile(fullPath, newContent, "utf-8");
13392
+ await fs27.writeFile(fullPath, newContent, "utf-8");
12839
13393
  }
12840
13394
  }
12841
13395
  }
@@ -13287,8 +13841,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
13287
13841
  }
13288
13842
  }
13289
13843
  }
13290
- return Array.from(noteMap.entries()).map(([path28, stats]) => ({
13291
- path: path28,
13844
+ return Array.from(noteMap.entries()).map(([path29, stats]) => ({
13845
+ path: path29,
13292
13846
  access_count: stats.access_count,
13293
13847
  last_accessed: stats.last_accessed,
13294
13848
  tools_used: Array.from(stats.tools)
@@ -13440,8 +13994,8 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
13440
13994
  import { z as z23 } from "zod";
13441
13995
 
13442
13996
  // src/core/read/similarity.ts
13443
- import * as fs27 from "fs";
13444
- import * as path26 from "path";
13997
+ import * as fs28 from "fs";
13998
+ import * as path27 from "path";
13445
13999
  var STOP_WORDS = /* @__PURE__ */ new Set([
13446
14000
  "the",
13447
14001
  "be",
@@ -13578,10 +14132,10 @@ function extractKeyTerms(content, maxTerms = 15) {
13578
14132
  }
13579
14133
  function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
13580
14134
  const limit = options.limit ?? 10;
13581
- const absPath = path26.join(vaultPath2, sourcePath);
14135
+ const absPath = path27.join(vaultPath2, sourcePath);
13582
14136
  let content;
13583
14137
  try {
13584
- content = fs27.readFileSync(absPath, "utf-8");
14138
+ content = fs28.readFileSync(absPath, "utf-8");
13585
14139
  } catch {
13586
14140
  return [];
13587
14141
  }
@@ -13706,7 +14260,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
13706
14260
  exclude_linked: z23.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
13707
14261
  }
13708
14262
  },
13709
- async ({ path: path28, limit, exclude_linked }) => {
14263
+ async ({ path: path29, limit, exclude_linked }) => {
13710
14264
  const index = getIndex();
13711
14265
  const vaultPath2 = getVaultPath();
13712
14266
  const stateDb2 = getStateDb();
@@ -13715,10 +14269,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
13715
14269
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
13716
14270
  };
13717
14271
  }
13718
- if (!index.notes.has(path28)) {
14272
+ if (!index.notes.has(path29)) {
13719
14273
  return {
13720
14274
  content: [{ type: "text", text: JSON.stringify({
13721
- error: `Note not found: ${path28}`,
14275
+ error: `Note not found: ${path29}`,
13722
14276
  hint: "Use the full relative path including .md extension"
13723
14277
  }, null, 2) }]
13724
14278
  };
@@ -13729,12 +14283,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
13729
14283
  };
13730
14284
  const useHybrid = hasEmbeddingsIndex();
13731
14285
  const method = useHybrid ? "hybrid" : "bm25";
13732
- const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path28, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path28, opts);
14286
+ const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path29, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path29, opts);
13733
14287
  return {
13734
14288
  content: [{
13735
14289
  type: "text",
13736
14290
  text: JSON.stringify({
13737
- source: path28,
14291
+ source: path29,
13738
14292
  method,
13739
14293
  exclude_linked: exclude_linked ?? true,
13740
14294
  count: results.length,
@@ -13748,6 +14302,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
13748
14302
 
13749
14303
  // src/tools/read/semantic.ts
13750
14304
  import { z as z24 } from "zod";
14305
+ import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
13751
14306
  function registerSemanticTools(server2, getVaultPath, getStateDb) {
13752
14307
  server2.registerTool(
13753
14308
  "init_semantic",
@@ -13792,6 +14347,29 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
13792
14347
  }
13793
14348
  });
13794
14349
  const embedded = progress.total - progress.skipped;
14350
+ let entityEmbedded = 0;
14351
+ try {
14352
+ const allEntities = getAllEntitiesFromDb(stateDb2);
14353
+ const entityMap = /* @__PURE__ */ new Map();
14354
+ for (const e of allEntities) {
14355
+ entityMap.set(e.name, {
14356
+ name: e.name,
14357
+ path: e.path,
14358
+ category: e.category,
14359
+ aliases: e.aliases
14360
+ });
14361
+ }
14362
+ if (entityMap.size > 0) {
14363
+ entityEmbedded = await buildEntityEmbeddingsIndex(vaultPath2, entityMap, (done, total) => {
14364
+ if (done % 50 === 0 || done === total) {
14365
+ console.error(`[Semantic] Entity embedding ${done}/${total}...`);
14366
+ }
14367
+ });
14368
+ loadEntityEmbeddingsToMemory();
14369
+ }
14370
+ } catch (err) {
14371
+ console.error("[Semantic] Entity embeddings failed:", err instanceof Error ? err.message : err);
14372
+ }
13795
14373
  return {
13796
14374
  content: [{
13797
14375
  type: "text",
@@ -13800,6 +14378,8 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
13800
14378
  embedded,
13801
14379
  skipped: progress.skipped,
13802
14380
  total: progress.total,
14381
+ entity_embeddings: entityEmbedded,
14382
+ entity_total: getEntityEmbeddingsCount(),
13803
14383
  hint: "Embeddings built. All searches now automatically use hybrid ranking."
13804
14384
  }, null, 2)
13805
14385
  }]
@@ -14181,6 +14761,7 @@ async function main() {
14181
14761
  console.error("[Memory] StateDb initialized");
14182
14762
  setFTS5Database(stateDb.db);
14183
14763
  setEmbeddingsDatabase(stateDb.db);
14764
+ loadEntityEmbeddingsToMemory();
14184
14765
  setWriteStateDb(stateDb);
14185
14766
  await initializeEntityIndex(vaultPath);
14186
14767
  } catch (err) {
@@ -14368,13 +14949,31 @@ async function runPostIndexWork(index) {
14368
14949
  if (event.type === "delete") {
14369
14950
  removeEmbedding(event.path);
14370
14951
  } else if (event.path.endsWith(".md")) {
14371
- const absPath = path27.join(vaultPath, event.path);
14952
+ const absPath = path28.join(vaultPath, event.path);
14372
14953
  await updateEmbedding(event.path, absPath);
14373
14954
  }
14374
14955
  } catch {
14375
14956
  }
14376
14957
  }
14377
14958
  }
14959
+ if (hasEntityEmbeddingsIndex() && stateDb) {
14960
+ try {
14961
+ const allEntities = getAllEntitiesFromDb2(stateDb);
14962
+ for (const event of batch.events) {
14963
+ if (event.type === "delete" || !event.path.endsWith(".md")) continue;
14964
+ const matching = allEntities.filter((e) => e.path === event.path);
14965
+ for (const entity of matching) {
14966
+ await updateEntityEmbedding(entity.name, {
14967
+ name: entity.name,
14968
+ path: entity.path,
14969
+ category: entity.category,
14970
+ aliases: entity.aliases
14971
+ }, vaultPath);
14972
+ }
14973
+ }
14974
+ } catch {
14975
+ }
14976
+ }
14378
14977
  if (stateDb) {
14379
14978
  try {
14380
14979
  saveVaultIndexToCache(stateDb, vaultIndex);