@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.
- package/dist/index.js +1187 -588
- 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
|
|
27
|
-
import
|
|
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
|
-
|
|
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 =
|
|
349
|
-
const resolvedNote =
|
|
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 =
|
|
378
|
-
const resolvedNote =
|
|
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 =
|
|
469
|
+
const fullPath = path17.join(vaultPath2, notePath);
|
|
393
470
|
try {
|
|
394
|
-
await
|
|
395
|
-
const realPath = await
|
|
396
|
-
const realVaultPath = await
|
|
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 =
|
|
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 =
|
|
488
|
+
const parentDir = path17.dirname(fullPath);
|
|
412
489
|
try {
|
|
413
|
-
await
|
|
414
|
-
const realParentPath = await
|
|
415
|
-
const realVaultPath = await
|
|
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 =
|
|
438
|
-
const rawContent = await
|
|
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 =
|
|
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
|
|
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,
|
|
715
|
-
const parts =
|
|
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
|
|
1154
|
-
import
|
|
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 =
|
|
1363
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
1208
1364
|
try {
|
|
1209
|
-
await
|
|
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 =
|
|
1378
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
1223
1379
|
try {
|
|
1224
|
-
await
|
|
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 =
|
|
1409
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
1254
1410
|
try {
|
|
1255
|
-
await
|
|
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 =
|
|
1440
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
1285
1441
|
try {
|
|
1286
|
-
await
|
|
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
|
|
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(
|
|
1694
|
-
return
|
|
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
|
|
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,
|
|
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:
|
|
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
|
|
1990
|
-
import * as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2596
|
+
const absFolder = path3.join(vaultPath2, templatesFolder);
|
|
2099
2597
|
try {
|
|
2100
|
-
const files =
|
|
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
|
|
2139
|
-
import * as
|
|
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 =
|
|
2640
|
+
let current = path4.resolve(startPath || process.cwd());
|
|
2143
2641
|
while (true) {
|
|
2144
2642
|
for (const marker of VAULT_MARKERS) {
|
|
2145
|
-
const markerPath =
|
|
2146
|
-
if (
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
2856
|
+
const path29 = normalizePath(rawPath);
|
|
2359
2857
|
const now = Date.now();
|
|
2360
2858
|
const event = {
|
|
2361
2859
|
type,
|
|
2362
|
-
path:
|
|
2860
|
+
path: path29,
|
|
2363
2861
|
timestamp: now
|
|
2364
2862
|
};
|
|
2365
|
-
let pending = this.pending.get(
|
|
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(
|
|
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 ${
|
|
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(
|
|
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(
|
|
2403
|
-
const pending = this.pending.get(
|
|
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 ${
|
|
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:
|
|
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(
|
|
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 [
|
|
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:
|
|
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", (
|
|
2592
|
-
console.error(`[flywheel] RAW EVENT: add ${
|
|
2593
|
-
if (shouldWatch(
|
|
2594
|
-
console.error(`[flywheel] ACCEPTED: add ${
|
|
2595
|
-
eventQueue.push("add",
|
|
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 ${
|
|
3095
|
+
console.error(`[flywheel] FILTERED: add ${path29}`);
|
|
2598
3096
|
}
|
|
2599
3097
|
});
|
|
2600
|
-
watcher.on("change", (
|
|
2601
|
-
console.error(`[flywheel] RAW EVENT: change ${
|
|
2602
|
-
if (shouldWatch(
|
|
2603
|
-
console.error(`[flywheel] ACCEPTED: change ${
|
|
2604
|
-
eventQueue.push("change",
|
|
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 ${
|
|
3104
|
+
console.error(`[flywheel] FILTERED: change ${path29}`);
|
|
2607
3105
|
}
|
|
2608
3106
|
});
|
|
2609
|
-
watcher.on("unlink", (
|
|
2610
|
-
console.error(`[flywheel] RAW EVENT: unlink ${
|
|
2611
|
-
if (shouldWatch(
|
|
2612
|
-
console.error(`[flywheel] ACCEPTED: unlink ${
|
|
2613
|
-
eventQueue.push("unlink",
|
|
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 ${
|
|
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
|
|
2906
|
-
if (
|
|
2907
|
-
accuracy =
|
|
2908
|
-
sampleCount =
|
|
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
|
|
2965
|
-
import
|
|
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 =
|
|
3516
|
+
const lockPath = path6.join(vaultPath2, ".git/index.lock");
|
|
3019
3517
|
try {
|
|
3020
|
-
const stat3 = await
|
|
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 =
|
|
3539
|
+
const lockPath = path6.join(vaultPath2, ".git/index.lock");
|
|
3042
3540
|
try {
|
|
3043
|
-
const stat3 = await
|
|
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 =
|
|
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
|
|
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 =
|
|
3308
|
-
const relativePath =
|
|
3309
|
-
const topFolder = relativePath.split(
|
|
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
|
|
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 =
|
|
4222
|
-
const relativePath =
|
|
4223
|
-
const topFolder = relativePath.split(
|
|
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
|
|
5135
|
-
var
|
|
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
|
|
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
|
-
|
|
5733
|
+
db2 = database;
|
|
5155
5734
|
try {
|
|
5156
|
-
const row =
|
|
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 =
|
|
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
|
|
5751
|
+
function shouldIndexFile2(filePath) {
|
|
5173
5752
|
const parts = filePath.split("/");
|
|
5174
|
-
return !parts.some((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 (!
|
|
5758
|
+
if (!db2) {
|
|
5180
5759
|
throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
|
|
5181
5760
|
}
|
|
5182
|
-
|
|
5761
|
+
db2.exec("DELETE FROM notes_fts");
|
|
5183
5762
|
const files = await scanVault(vaultPath2);
|
|
5184
|
-
const indexableFiles = files.filter((f) =>
|
|
5185
|
-
const insert =
|
|
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 =
|
|
5767
|
+
const insertMany = db2.transaction((filesToIndex) => {
|
|
5189
5768
|
let indexed2 = 0;
|
|
5190
5769
|
for (const file of filesToIndex) {
|
|
5191
5770
|
try {
|
|
5192
|
-
const stats =
|
|
5771
|
+
const stats = fs7.statSync(file.absolutePath);
|
|
5193
5772
|
if (stats.size > MAX_INDEX_FILE_SIZE) {
|
|
5194
5773
|
continue;
|
|
5195
5774
|
}
|
|
5196
|
-
const content =
|
|
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
|
-
|
|
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
|
-
|
|
5808
|
+
function isIndexStale(_vaultPath) {
|
|
5433
5809
|
if (!db2) {
|
|
5434
|
-
|
|
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(
|
|
5477
|
-
|
|
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
|
|
5823
|
+
return true;
|
|
5480
5824
|
}
|
|
5481
5825
|
}
|
|
5482
|
-
function
|
|
5483
|
-
if (!db2)
|
|
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
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
6447
|
+
for (const [name, path29] of entities) {
|
|
6086
6448
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
6087
|
-
return
|
|
6449
|
+
return path29;
|
|
6088
6450
|
}
|
|
6089
6451
|
}
|
|
6090
|
-
for (const [name,
|
|
6452
|
+
for (const [name, path29] of entities) {
|
|
6091
6453
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
6092
|
-
return
|
|
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(
|
|
6669
|
-
const filename =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
8099
|
+
async ({ path: path29, include_content }) => {
|
|
7738
8100
|
const index = getIndex();
|
|
7739
8101
|
const vaultPath2 = getVaultPath();
|
|
7740
|
-
const result = await getNoteStructure(index,
|
|
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:
|
|
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,
|
|
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:
|
|
8132
|
+
async ({ path: path29, heading, include_subheadings }) => {
|
|
7771
8133
|
const index = getIndex();
|
|
7772
8134
|
const vaultPath2 = getVaultPath();
|
|
7773
|
-
const result = await getSectionContent(index,
|
|
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:
|
|
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:
|
|
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 (
|
|
7838
|
-
const result2 = await getTasksFromNote(index,
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
10042
|
+
import * as path16 from "path";
|
|
9559
10043
|
import matter4 from "gray-matter";
|
|
9560
10044
|
async function readFileContent3(notePath, vaultPath2) {
|
|
9561
|
-
const fullPath =
|
|
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 =
|
|
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
|
|
9782
|
-
import
|
|
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
|
|
9985
|
-
import
|
|
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 =
|
|
10569
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
10035
10570
|
try {
|
|
10036
|
-
await
|
|
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 =
|
|
10135
|
-
await
|
|
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 =
|
|
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 =
|
|
10159
|
-
templateContent = await
|
|
10698
|
+
const absTemplatePath = path19.join(vaultPath2, templatePath);
|
|
10699
|
+
templateContent = await fs20.readFile(absTemplatePath, "utf-8");
|
|
10160
10700
|
} catch {
|
|
10161
|
-
const title =
|
|
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 =
|
|
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,
|
|
10180
|
-
await
|
|
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 =
|
|
10752
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
10212
10753
|
try {
|
|
10213
|
-
await
|
|
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
|
-
|
|
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
|
|
10649
|
-
import
|
|
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 =
|
|
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 =
|
|
10678
|
-
await
|
|
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 =
|
|
11236
|
+
const templatePath = path20.join(vaultPath2, template);
|
|
10683
11237
|
try {
|
|
10684
|
-
const raw = await
|
|
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 =
|
|
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 =
|
|
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 =
|
|
10818
|
-
await
|
|
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
|
|
10837
|
-
import
|
|
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
|
|
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
|
|
11417
|
+
const entries = await fs22.readdir(dir, { withFileTypes: true });
|
|
10864
11418
|
for (const entry of entries) {
|
|
10865
|
-
const fullPath =
|
|
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 =
|
|
10877
|
-
const content = await
|
|
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 =
|
|
10897
|
-
const raw = await
|
|
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 =
|
|
10964
|
-
const newFullPath =
|
|
11517
|
+
const oldFullPath = path21.join(vaultPath2, oldPath);
|
|
11518
|
+
const newFullPath = path21.join(vaultPath2, newPath);
|
|
10965
11519
|
try {
|
|
10966
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
11015
|
-
await
|
|
11016
|
-
await
|
|
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 =
|
|
11101
|
-
const dir =
|
|
11102
|
-
const newPath = dir === "." ? `${sanitizedTitle}.md` :
|
|
11103
|
-
const newFullPath =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11329
|
-
import
|
|
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
|
|
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 =
|
|
11379
|
-
const policyPath =
|
|
11932
|
+
const policiesDir = path22.join(vaultPath2, ".claude", "policies");
|
|
11933
|
+
const policyPath = path22.join(policiesDir, `${policyName}.yaml`);
|
|
11380
11934
|
try {
|
|
11381
|
-
await
|
|
11935
|
+
await fs23.access(policyPath);
|
|
11382
11936
|
return loadPolicyFile(policyPath);
|
|
11383
11937
|
} catch {
|
|
11384
|
-
const ymlPath =
|
|
11938
|
+
const ymlPath = path22.join(policiesDir, `${policyName}.yml`);
|
|
11385
11939
|
try {
|
|
11386
|
-
await
|
|
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
|
|
11525
|
-
import
|
|
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 =
|
|
12148
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
11595
12149
|
try {
|
|
11596
|
-
await
|
|
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 =
|
|
12188
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
11635
12189
|
try {
|
|
11636
|
-
await
|
|
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 =
|
|
12219
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
11666
12220
|
try {
|
|
11667
|
-
await
|
|
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 =
|
|
12262
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
11709
12263
|
try {
|
|
11710
|
-
await
|
|
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 =
|
|
11717
|
-
await
|
|
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 =
|
|
12290
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
11737
12291
|
try {
|
|
11738
|
-
await
|
|
12292
|
+
await fs25.access(fullPath);
|
|
11739
12293
|
} catch {
|
|
11740
12294
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11741
12295
|
}
|
|
11742
|
-
await
|
|
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 =
|
|
12307
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
11754
12308
|
try {
|
|
11755
|
-
await
|
|
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 =
|
|
12350
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
11797
12351
|
try {
|
|
11798
|
-
await
|
|
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 =
|
|
12387
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
11834
12388
|
try {
|
|
11835
|
-
await
|
|
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 =
|
|
12409
|
+
const fullPath = path24.join(vaultPath2, notePath);
|
|
11856
12410
|
try {
|
|
11857
|
-
await
|
|
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 =
|
|
12569
|
+
const fullPath = path24.join(vaultPath2, filePath);
|
|
12016
12570
|
if (original === null) {
|
|
12017
12571
|
try {
|
|
12018
|
-
await
|
|
12572
|
+
await fs25.unlink(fullPath);
|
|
12019
12573
|
} catch {
|
|
12020
12574
|
}
|
|
12021
12575
|
} else if (original !== void 0) {
|
|
12022
12576
|
try {
|
|
12023
|
-
await
|
|
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
|
|
12070
|
-
import
|
|
12623
|
+
import fs26 from "fs/promises";
|
|
12624
|
+
import path25 from "path";
|
|
12071
12625
|
function getPoliciesDir(vaultPath2) {
|
|
12072
|
-
return
|
|
12626
|
+
return path25.join(vaultPath2, ".claude", "policies");
|
|
12073
12627
|
}
|
|
12074
12628
|
async function ensurePoliciesDir(vaultPath2) {
|
|
12075
12629
|
const dir = getPoliciesDir(vaultPath2);
|
|
12076
|
-
await
|
|
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
|
|
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 =
|
|
12088
|
-
const stat3 = await
|
|
12089
|
-
const content = await
|
|
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 =
|
|
12666
|
+
const filePath = path25.join(dir, filename);
|
|
12113
12667
|
if (!overwrite) {
|
|
12114
12668
|
try {
|
|
12115
|
-
await
|
|
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
|
|
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
|
|
12656
|
-
import * as
|
|
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 =
|
|
13316
|
+
const fullPath = path26.join(vaultPath2, note.path);
|
|
12763
13317
|
let fileContent;
|
|
12764
13318
|
try {
|
|
12765
|
-
fileContent = await
|
|
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
|
|
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(([
|
|
13291
|
-
path:
|
|
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
|
|
13444
|
-
import * as
|
|
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 =
|
|
14135
|
+
const absPath = path27.join(vaultPath2, sourcePath);
|
|
13582
14136
|
let content;
|
|
13583
14137
|
try {
|
|
13584
|
-
content =
|
|
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:
|
|
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(
|
|
14272
|
+
if (!index.notes.has(path29)) {
|
|
13719
14273
|
return {
|
|
13720
14274
|
content: [{ type: "text", text: JSON.stringify({
|
|
13721
|
-
error: `Note not found: ${
|
|
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,
|
|
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:
|
|
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 =
|
|
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);
|