@velvetmonkey/flywheel-memory 2.0.34 → 2.0.36
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 +917 -247
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ var init_constants = __esm({
|
|
|
59
59
|
|
|
60
60
|
// src/core/write/writer.ts
|
|
61
61
|
import fs18 from "fs/promises";
|
|
62
|
-
import
|
|
62
|
+
import path19 from "path";
|
|
63
63
|
import matter5 from "gray-matter";
|
|
64
64
|
function isSensitivePath(filePath) {
|
|
65
65
|
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
@@ -386,8 +386,8 @@ function validatePath(vaultPath2, notePath) {
|
|
|
386
386
|
if (notePath.startsWith("\\")) {
|
|
387
387
|
return false;
|
|
388
388
|
}
|
|
389
|
-
const resolvedVault =
|
|
390
|
-
const resolvedNote =
|
|
389
|
+
const resolvedVault = path19.resolve(vaultPath2);
|
|
390
|
+
const resolvedNote = path19.resolve(vaultPath2, notePath);
|
|
391
391
|
return resolvedNote.startsWith(resolvedVault);
|
|
392
392
|
}
|
|
393
393
|
async function validatePathSecure(vaultPath2, notePath) {
|
|
@@ -415,8 +415,8 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
415
415
|
reason: "Path traversal not allowed"
|
|
416
416
|
};
|
|
417
417
|
}
|
|
418
|
-
const resolvedVault =
|
|
419
|
-
const resolvedNote =
|
|
418
|
+
const resolvedVault = path19.resolve(vaultPath2);
|
|
419
|
+
const resolvedNote = path19.resolve(vaultPath2, notePath);
|
|
420
420
|
if (!resolvedNote.startsWith(resolvedVault)) {
|
|
421
421
|
return {
|
|
422
422
|
valid: false,
|
|
@@ -430,7 +430,7 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
430
430
|
};
|
|
431
431
|
}
|
|
432
432
|
try {
|
|
433
|
-
const fullPath =
|
|
433
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
434
434
|
try {
|
|
435
435
|
await fs18.access(fullPath);
|
|
436
436
|
const realPath = await fs18.realpath(fullPath);
|
|
@@ -441,7 +441,7 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
441
441
|
reason: "Symlink target is outside vault"
|
|
442
442
|
};
|
|
443
443
|
}
|
|
444
|
-
const relativePath =
|
|
444
|
+
const relativePath = path19.relative(realVaultPath, realPath);
|
|
445
445
|
if (isSensitivePath(relativePath)) {
|
|
446
446
|
return {
|
|
447
447
|
valid: false,
|
|
@@ -449,7 +449,7 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
449
449
|
};
|
|
450
450
|
}
|
|
451
451
|
} catch {
|
|
452
|
-
const parentDir =
|
|
452
|
+
const parentDir = path19.dirname(fullPath);
|
|
453
453
|
try {
|
|
454
454
|
await fs18.access(parentDir);
|
|
455
455
|
const realParentPath = await fs18.realpath(parentDir);
|
|
@@ -475,7 +475,7 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
475
475
|
if (!validatePath(vaultPath2, notePath)) {
|
|
476
476
|
throw new Error("Invalid path: path traversal not allowed");
|
|
477
477
|
}
|
|
478
|
-
const fullPath =
|
|
478
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
479
479
|
const [rawContent, stat3] = await Promise.all([
|
|
480
480
|
fs18.readFile(fullPath, "utf-8"),
|
|
481
481
|
fs18.stat(fullPath)
|
|
@@ -528,7 +528,7 @@ async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEn
|
|
|
528
528
|
if (!validation.valid) {
|
|
529
529
|
throw new Error(`Invalid path: ${validation.reason}`);
|
|
530
530
|
}
|
|
531
|
-
const fullPath =
|
|
531
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
532
532
|
let output = matter5.stringify(content, frontmatter);
|
|
533
533
|
output = normalizeTrailingNewline(output);
|
|
534
534
|
output = convertLineEndings(output, lineEnding);
|
|
@@ -832,11 +832,12 @@ function createContext(variables = {}) {
|
|
|
832
832
|
today: formatDate2(now),
|
|
833
833
|
time: formatTime(now),
|
|
834
834
|
date: formatDate2(now)
|
|
835
|
-
}
|
|
835
|
+
},
|
|
836
|
+
steps: {}
|
|
836
837
|
};
|
|
837
838
|
}
|
|
838
|
-
function resolvePath(obj,
|
|
839
|
-
const parts =
|
|
839
|
+
function resolvePath(obj, path31) {
|
|
840
|
+
const parts = path31.split(".");
|
|
840
841
|
let current = obj;
|
|
841
842
|
for (const part of parts) {
|
|
842
843
|
if (current === void 0 || current === null) {
|
|
@@ -878,6 +879,9 @@ function resolveExpression(expr, context) {
|
|
|
878
879
|
if (trimmed.startsWith("builtins.")) {
|
|
879
880
|
return resolvePath(context.builtins, trimmed.slice("builtins.".length));
|
|
880
881
|
}
|
|
882
|
+
if (trimmed.startsWith("steps.")) {
|
|
883
|
+
return resolvePath(context.steps, trimmed.slice("steps.".length));
|
|
884
|
+
}
|
|
881
885
|
return resolvePath(context.variables, trimmed);
|
|
882
886
|
}
|
|
883
887
|
function interpolate(template, context) {
|
|
@@ -1056,7 +1060,7 @@ function validatePolicySchema(policy) {
|
|
|
1056
1060
|
const match = ref.match(/\{\{(?:variables\.)?(\w+)/);
|
|
1057
1061
|
if (match) {
|
|
1058
1062
|
const varName = match[1];
|
|
1059
|
-
if (["now", "today", "time", "date", "conditions"].includes(varName)) {
|
|
1063
|
+
if (["now", "today", "time", "date", "conditions", "steps"].includes(varName)) {
|
|
1060
1064
|
continue;
|
|
1061
1065
|
}
|
|
1062
1066
|
if (!varNames.has(varName)) {
|
|
@@ -1275,7 +1279,7 @@ __export(conditions_exports, {
|
|
|
1275
1279
|
shouldStepExecute: () => shouldStepExecute
|
|
1276
1280
|
});
|
|
1277
1281
|
import fs25 from "fs/promises";
|
|
1278
|
-
import
|
|
1282
|
+
import path25 from "path";
|
|
1279
1283
|
async function evaluateCondition(condition, vaultPath2, context) {
|
|
1280
1284
|
const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
|
|
1281
1285
|
const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
|
|
@@ -1328,7 +1332,7 @@ async function evaluateCondition(condition, vaultPath2, context) {
|
|
|
1328
1332
|
}
|
|
1329
1333
|
}
|
|
1330
1334
|
async function evaluateFileExists(vaultPath2, notePath, expectExists) {
|
|
1331
|
-
const fullPath =
|
|
1335
|
+
const fullPath = path25.join(vaultPath2, notePath);
|
|
1332
1336
|
try {
|
|
1333
1337
|
await fs25.access(fullPath);
|
|
1334
1338
|
return {
|
|
@@ -1343,7 +1347,7 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
|
|
|
1343
1347
|
}
|
|
1344
1348
|
}
|
|
1345
1349
|
async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
|
|
1346
|
-
const fullPath =
|
|
1350
|
+
const fullPath = path25.join(vaultPath2, notePath);
|
|
1347
1351
|
try {
|
|
1348
1352
|
await fs25.access(fullPath);
|
|
1349
1353
|
} catch {
|
|
@@ -1374,7 +1378,7 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
|
|
|
1374
1378
|
}
|
|
1375
1379
|
}
|
|
1376
1380
|
async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
|
|
1377
|
-
const fullPath =
|
|
1381
|
+
const fullPath = path25.join(vaultPath2, notePath);
|
|
1378
1382
|
try {
|
|
1379
1383
|
await fs25.access(fullPath);
|
|
1380
1384
|
} catch {
|
|
@@ -1405,7 +1409,7 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
|
|
|
1405
1409
|
}
|
|
1406
1410
|
}
|
|
1407
1411
|
async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
|
|
1408
|
-
const fullPath =
|
|
1412
|
+
const fullPath = path25.join(vaultPath2, notePath);
|
|
1409
1413
|
try {
|
|
1410
1414
|
await fs25.access(fullPath);
|
|
1411
1415
|
} catch {
|
|
@@ -1548,7 +1552,10 @@ var init_taskHelpers = __esm({
|
|
|
1548
1552
|
});
|
|
1549
1553
|
|
|
1550
1554
|
// src/index.ts
|
|
1551
|
-
import * as
|
|
1555
|
+
import * as path30 from "path";
|
|
1556
|
+
import { readFileSync as readFileSync4, realpathSync } from "fs";
|
|
1557
|
+
import { fileURLToPath } from "url";
|
|
1558
|
+
import { dirname as dirname4, join as join16 } from "path";
|
|
1552
1559
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1553
1560
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1554
1561
|
|
|
@@ -2209,8 +2216,8 @@ function updateIndexProgress(parsed, total) {
|
|
|
2209
2216
|
function normalizeTarget(target) {
|
|
2210
2217
|
return target.toLowerCase().replace(/\.md$/, "");
|
|
2211
2218
|
}
|
|
2212
|
-
function normalizeNotePath(
|
|
2213
|
-
return
|
|
2219
|
+
function normalizeNotePath(path31) {
|
|
2220
|
+
return path31.toLowerCase().replace(/\.md$/, "");
|
|
2214
2221
|
}
|
|
2215
2222
|
async function buildVaultIndex(vaultPath2, options = {}) {
|
|
2216
2223
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
|
|
@@ -2260,7 +2267,10 @@ async function buildVaultIndexInternal(vaultPath2, startTime, onProgress) {
|
|
|
2260
2267
|
}
|
|
2261
2268
|
}
|
|
2262
2269
|
if (parseErrors.length > 0) {
|
|
2263
|
-
console.error(`Failed to parse ${parseErrors.length} files
|
|
2270
|
+
console.error(`Failed to parse ${parseErrors.length} files:`);
|
|
2271
|
+
for (const errorPath of parseErrors) {
|
|
2272
|
+
console.error(` - ${errorPath}`);
|
|
2273
|
+
}
|
|
2264
2274
|
}
|
|
2265
2275
|
const entities = /* @__PURE__ */ new Map();
|
|
2266
2276
|
for (const note of notes.values()) {
|
|
@@ -2376,7 +2386,7 @@ function findSimilarEntity(index, target) {
|
|
|
2376
2386
|
}
|
|
2377
2387
|
const maxDist = normalizedLen <= 10 ? 1 : 2;
|
|
2378
2388
|
let bestMatch;
|
|
2379
|
-
for (const [entity,
|
|
2389
|
+
for (const [entity, path31] of index.entities) {
|
|
2380
2390
|
const lenDiff = Math.abs(entity.length - normalizedLen);
|
|
2381
2391
|
if (lenDiff > maxDist) {
|
|
2382
2392
|
continue;
|
|
@@ -2384,7 +2394,7 @@ function findSimilarEntity(index, target) {
|
|
|
2384
2394
|
const dist = levenshteinDistance(normalized, entity);
|
|
2385
2395
|
if (dist > 0 && dist <= maxDist) {
|
|
2386
2396
|
if (!bestMatch || dist < bestMatch.distance) {
|
|
2387
|
-
bestMatch = { path:
|
|
2397
|
+
bestMatch = { path: path31, entity, distance: dist };
|
|
2388
2398
|
if (dist === 1) {
|
|
2389
2399
|
return bestMatch;
|
|
2390
2400
|
}
|
|
@@ -2854,30 +2864,30 @@ var EventQueue = class {
|
|
|
2854
2864
|
* Add a new event to the queue
|
|
2855
2865
|
*/
|
|
2856
2866
|
push(type, rawPath) {
|
|
2857
|
-
const
|
|
2867
|
+
const path31 = normalizePath(rawPath);
|
|
2858
2868
|
const now = Date.now();
|
|
2859
2869
|
const event = {
|
|
2860
2870
|
type,
|
|
2861
|
-
path:
|
|
2871
|
+
path: path31,
|
|
2862
2872
|
timestamp: now
|
|
2863
2873
|
};
|
|
2864
|
-
let pending = this.pending.get(
|
|
2874
|
+
let pending = this.pending.get(path31);
|
|
2865
2875
|
if (!pending) {
|
|
2866
2876
|
pending = {
|
|
2867
2877
|
events: [],
|
|
2868
2878
|
timer: null,
|
|
2869
2879
|
lastEvent: now
|
|
2870
2880
|
};
|
|
2871
|
-
this.pending.set(
|
|
2881
|
+
this.pending.set(path31, pending);
|
|
2872
2882
|
}
|
|
2873
2883
|
pending.events.push(event);
|
|
2874
2884
|
pending.lastEvent = now;
|
|
2875
|
-
console.error(`[flywheel] QUEUE: pushed ${type} for ${
|
|
2885
|
+
console.error(`[flywheel] QUEUE: pushed ${type} for ${path31}, pending=${this.pending.size}`);
|
|
2876
2886
|
if (pending.timer) {
|
|
2877
2887
|
clearTimeout(pending.timer);
|
|
2878
2888
|
}
|
|
2879
2889
|
pending.timer = setTimeout(() => {
|
|
2880
|
-
this.flushPath(
|
|
2890
|
+
this.flushPath(path31);
|
|
2881
2891
|
}, this.config.debounceMs);
|
|
2882
2892
|
if (this.pending.size >= this.config.batchSize) {
|
|
2883
2893
|
this.flush();
|
|
@@ -2898,10 +2908,10 @@ var EventQueue = class {
|
|
|
2898
2908
|
/**
|
|
2899
2909
|
* Flush a single path's events
|
|
2900
2910
|
*/
|
|
2901
|
-
flushPath(
|
|
2902
|
-
const pending = this.pending.get(
|
|
2911
|
+
flushPath(path31) {
|
|
2912
|
+
const pending = this.pending.get(path31);
|
|
2903
2913
|
if (!pending || pending.events.length === 0) return;
|
|
2904
|
-
console.error(`[flywheel] QUEUE: flushing ${
|
|
2914
|
+
console.error(`[flywheel] QUEUE: flushing ${path31}, events=${pending.events.length}`);
|
|
2905
2915
|
if (pending.timer) {
|
|
2906
2916
|
clearTimeout(pending.timer);
|
|
2907
2917
|
pending.timer = null;
|
|
@@ -2910,7 +2920,7 @@ var EventQueue = class {
|
|
|
2910
2920
|
if (coalescedType) {
|
|
2911
2921
|
const coalesced = {
|
|
2912
2922
|
type: coalescedType,
|
|
2913
|
-
path:
|
|
2923
|
+
path: path31,
|
|
2914
2924
|
originalEvents: [...pending.events]
|
|
2915
2925
|
};
|
|
2916
2926
|
this.onBatch({
|
|
@@ -2918,7 +2928,7 @@ var EventQueue = class {
|
|
|
2918
2928
|
timestamp: Date.now()
|
|
2919
2929
|
});
|
|
2920
2930
|
}
|
|
2921
|
-
this.pending.delete(
|
|
2931
|
+
this.pending.delete(path31);
|
|
2922
2932
|
}
|
|
2923
2933
|
/**
|
|
2924
2934
|
* Flush all pending events
|
|
@@ -2930,7 +2940,7 @@ var EventQueue = class {
|
|
|
2930
2940
|
}
|
|
2931
2941
|
if (this.pending.size === 0) return;
|
|
2932
2942
|
const events = [];
|
|
2933
|
-
for (const [
|
|
2943
|
+
for (const [path31, pending] of this.pending) {
|
|
2934
2944
|
if (pending.timer) {
|
|
2935
2945
|
clearTimeout(pending.timer);
|
|
2936
2946
|
}
|
|
@@ -2938,7 +2948,7 @@ var EventQueue = class {
|
|
|
2938
2948
|
if (coalescedType) {
|
|
2939
2949
|
events.push({
|
|
2940
2950
|
type: coalescedType,
|
|
2941
|
-
path:
|
|
2951
|
+
path: path31,
|
|
2942
2952
|
originalEvents: [...pending.events]
|
|
2943
2953
|
});
|
|
2944
2954
|
}
|
|
@@ -3013,6 +3023,208 @@ function parseWatcherConfig() {
|
|
|
3013
3023
|
};
|
|
3014
3024
|
}
|
|
3015
3025
|
|
|
3026
|
+
// src/core/read/watch/incrementalIndex.ts
|
|
3027
|
+
import path6 from "path";
|
|
3028
|
+
function normalizeTarget2(target) {
|
|
3029
|
+
return target.toLowerCase().replace(/\.md$/, "");
|
|
3030
|
+
}
|
|
3031
|
+
function normalizeNotePath2(notePath) {
|
|
3032
|
+
return notePath.toLowerCase().replace(/\.md$/, "");
|
|
3033
|
+
}
|
|
3034
|
+
function removeNoteFromIndex(index, notePath) {
|
|
3035
|
+
const note = index.notes.get(notePath);
|
|
3036
|
+
if (!note) {
|
|
3037
|
+
return false;
|
|
3038
|
+
}
|
|
3039
|
+
index.notes.delete(notePath);
|
|
3040
|
+
const normalizedTitle = normalizeTarget2(note.title);
|
|
3041
|
+
const normalizedPath = normalizeNotePath2(notePath);
|
|
3042
|
+
if (index.entities.get(normalizedTitle) === notePath) {
|
|
3043
|
+
index.entities.delete(normalizedTitle);
|
|
3044
|
+
}
|
|
3045
|
+
if (index.entities.get(normalizedPath) === notePath) {
|
|
3046
|
+
index.entities.delete(normalizedPath);
|
|
3047
|
+
}
|
|
3048
|
+
for (const alias of note.aliases) {
|
|
3049
|
+
const normalizedAlias = normalizeTarget2(alias);
|
|
3050
|
+
if (index.entities.get(normalizedAlias) === notePath) {
|
|
3051
|
+
index.entities.delete(normalizedAlias);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
for (const tag of note.tags) {
|
|
3055
|
+
const tagPaths = index.tags.get(tag);
|
|
3056
|
+
if (tagPaths) {
|
|
3057
|
+
tagPaths.delete(notePath);
|
|
3058
|
+
if (tagPaths.size === 0) {
|
|
3059
|
+
index.tags.delete(tag);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
for (const link of note.outlinks) {
|
|
3064
|
+
const normalizedTarget = normalizeTarget2(link.target);
|
|
3065
|
+
const targetPath = index.entities.get(normalizedTarget);
|
|
3066
|
+
const key = targetPath ? normalizeNotePath2(targetPath) : normalizedTarget;
|
|
3067
|
+
const backlinks = index.backlinks.get(key);
|
|
3068
|
+
if (backlinks) {
|
|
3069
|
+
const filtered = backlinks.filter((bl) => bl.source !== notePath);
|
|
3070
|
+
if (filtered.length === 0) {
|
|
3071
|
+
index.backlinks.delete(key);
|
|
3072
|
+
} else {
|
|
3073
|
+
index.backlinks.set(key, filtered);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
return true;
|
|
3078
|
+
}
|
|
3079
|
+
function addNoteToIndex(index, note) {
|
|
3080
|
+
index.notes.set(note.path, note);
|
|
3081
|
+
const normalizedTitle = normalizeTarget2(note.title);
|
|
3082
|
+
const normalizedPath = normalizeNotePath2(note.path);
|
|
3083
|
+
if (!index.entities.has(normalizedTitle)) {
|
|
3084
|
+
index.entities.set(normalizedTitle, note.path);
|
|
3085
|
+
}
|
|
3086
|
+
index.entities.set(normalizedPath, note.path);
|
|
3087
|
+
for (const alias of note.aliases) {
|
|
3088
|
+
const normalizedAlias = normalizeTarget2(alias);
|
|
3089
|
+
if (!index.entities.has(normalizedAlias)) {
|
|
3090
|
+
index.entities.set(normalizedAlias, note.path);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
for (const tag of note.tags) {
|
|
3094
|
+
if (!index.tags.has(tag)) {
|
|
3095
|
+
index.tags.set(tag, /* @__PURE__ */ new Set());
|
|
3096
|
+
}
|
|
3097
|
+
index.tags.get(tag).add(note.path);
|
|
3098
|
+
}
|
|
3099
|
+
for (const link of note.outlinks) {
|
|
3100
|
+
const normalizedTarget = normalizeTarget2(link.target);
|
|
3101
|
+
const targetPath = index.entities.get(normalizedTarget);
|
|
3102
|
+
const key = targetPath ? normalizeNotePath2(targetPath) : normalizedTarget;
|
|
3103
|
+
if (!index.backlinks.has(key)) {
|
|
3104
|
+
index.backlinks.set(key, []);
|
|
3105
|
+
}
|
|
3106
|
+
index.backlinks.get(key).push({
|
|
3107
|
+
source: note.path,
|
|
3108
|
+
line: link.line
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
async function upsertNote(index, vaultPath2, notePath) {
|
|
3113
|
+
try {
|
|
3114
|
+
const existed = index.notes.has(notePath);
|
|
3115
|
+
if (existed) {
|
|
3116
|
+
removeNoteFromIndex(index, notePath);
|
|
3117
|
+
}
|
|
3118
|
+
const fullPath = path6.join(vaultPath2, notePath);
|
|
3119
|
+
const fs31 = await import("fs/promises");
|
|
3120
|
+
const stats = await fs31.stat(fullPath);
|
|
3121
|
+
const vaultFile = {
|
|
3122
|
+
path: notePath,
|
|
3123
|
+
absolutePath: fullPath,
|
|
3124
|
+
modified: stats.mtime
|
|
3125
|
+
};
|
|
3126
|
+
const note = await parseNote(vaultFile);
|
|
3127
|
+
addNoteToIndex(index, note);
|
|
3128
|
+
return {
|
|
3129
|
+
success: true,
|
|
3130
|
+
action: existed ? "updated" : "added",
|
|
3131
|
+
path: notePath
|
|
3132
|
+
};
|
|
3133
|
+
} catch (error) {
|
|
3134
|
+
return {
|
|
3135
|
+
success: false,
|
|
3136
|
+
action: "unchanged",
|
|
3137
|
+
path: notePath,
|
|
3138
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
3139
|
+
};
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
function deleteNote(index, notePath) {
|
|
3143
|
+
const removed = removeNoteFromIndex(index, notePath);
|
|
3144
|
+
return {
|
|
3145
|
+
success: removed,
|
|
3146
|
+
action: removed ? "removed" : "unchanged",
|
|
3147
|
+
path: notePath
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// src/core/read/watch/batchProcessor.ts
|
|
3152
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
3153
|
+
var YIELD_INTERVAL = 10;
|
|
3154
|
+
async function processBatch(index, vaultPath2, batch, options = {}) {
|
|
3155
|
+
const { concurrency = DEFAULT_CONCURRENCY, onProgress, onError } = options;
|
|
3156
|
+
const startTime = Date.now();
|
|
3157
|
+
const results = [];
|
|
3158
|
+
let successful = 0;
|
|
3159
|
+
let failed = 0;
|
|
3160
|
+
let processed = 0;
|
|
3161
|
+
const events = batch.events;
|
|
3162
|
+
const total = events.length;
|
|
3163
|
+
if (total === 0) {
|
|
3164
|
+
return {
|
|
3165
|
+
total: 0,
|
|
3166
|
+
successful: 0,
|
|
3167
|
+
failed: 0,
|
|
3168
|
+
results: [],
|
|
3169
|
+
durationMs: 0
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
console.error(`[flywheel] Processing ${total} file events`);
|
|
3173
|
+
for (let i = 0; i < events.length; i += concurrency) {
|
|
3174
|
+
const chunk = events.slice(i, i + concurrency);
|
|
3175
|
+
const chunkResults = await Promise.allSettled(
|
|
3176
|
+
chunk.map(async (event) => {
|
|
3177
|
+
const relativePath = getRelativePath(vaultPath2, event.path);
|
|
3178
|
+
if (event.type === "delete") {
|
|
3179
|
+
return deleteNote(index, relativePath);
|
|
3180
|
+
} else {
|
|
3181
|
+
return upsertNote(index, vaultPath2, relativePath);
|
|
3182
|
+
}
|
|
3183
|
+
})
|
|
3184
|
+
);
|
|
3185
|
+
for (let j = 0; j < chunkResults.length; j++) {
|
|
3186
|
+
const result = chunkResults[j];
|
|
3187
|
+
processed++;
|
|
3188
|
+
if (result.status === "fulfilled") {
|
|
3189
|
+
results.push(result.value);
|
|
3190
|
+
if (result.value.success) {
|
|
3191
|
+
successful++;
|
|
3192
|
+
} else {
|
|
3193
|
+
failed++;
|
|
3194
|
+
if (result.value.error && onError) {
|
|
3195
|
+
onError(result.value.path, result.value.error);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
} else {
|
|
3199
|
+
failed++;
|
|
3200
|
+
const event = chunk[j];
|
|
3201
|
+
const relativePath = getRelativePath(vaultPath2, event.path);
|
|
3202
|
+
const error = result.reason instanceof Error ? result.reason : new Error(String(result.reason));
|
|
3203
|
+
results.push({
|
|
3204
|
+
success: false,
|
|
3205
|
+
action: "unchanged",
|
|
3206
|
+
path: relativePath,
|
|
3207
|
+
error
|
|
3208
|
+
});
|
|
3209
|
+
onError?.(relativePath, error);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
onProgress?.(processed, total);
|
|
3213
|
+
if (processed % YIELD_INTERVAL === 0 && processed < total) {
|
|
3214
|
+
await new Promise((resolve2) => setImmediate(resolve2));
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
const durationMs = Date.now() - startTime;
|
|
3218
|
+
console.error(`[flywheel] Processed ${successful}/${total} files in ${durationMs}ms`);
|
|
3219
|
+
return {
|
|
3220
|
+
total,
|
|
3221
|
+
successful,
|
|
3222
|
+
failed,
|
|
3223
|
+
results,
|
|
3224
|
+
durationMs
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3016
3228
|
// src/core/read/watch/index.ts
|
|
3017
3229
|
function createVaultWatcher(options) {
|
|
3018
3230
|
const { vaultPath: vaultPath2, onBatch, onStateChange, onError } = options;
|
|
@@ -3087,31 +3299,31 @@ function createVaultWatcher(options) {
|
|
|
3087
3299
|
usePolling: config.usePolling,
|
|
3088
3300
|
interval: config.usePolling ? config.pollInterval : void 0
|
|
3089
3301
|
});
|
|
3090
|
-
watcher.on("add", (
|
|
3091
|
-
console.error(`[flywheel] RAW EVENT: add ${
|
|
3092
|
-
if (shouldWatch(
|
|
3093
|
-
console.error(`[flywheel] ACCEPTED: add ${
|
|
3094
|
-
eventQueue.push("add",
|
|
3302
|
+
watcher.on("add", (path31) => {
|
|
3303
|
+
console.error(`[flywheel] RAW EVENT: add ${path31}`);
|
|
3304
|
+
if (shouldWatch(path31, vaultPath2)) {
|
|
3305
|
+
console.error(`[flywheel] ACCEPTED: add ${path31}`);
|
|
3306
|
+
eventQueue.push("add", path31);
|
|
3095
3307
|
} else {
|
|
3096
|
-
console.error(`[flywheel] FILTERED: add ${
|
|
3308
|
+
console.error(`[flywheel] FILTERED: add ${path31}`);
|
|
3097
3309
|
}
|
|
3098
3310
|
});
|
|
3099
|
-
watcher.on("change", (
|
|
3100
|
-
console.error(`[flywheel] RAW EVENT: change ${
|
|
3101
|
-
if (shouldWatch(
|
|
3102
|
-
console.error(`[flywheel] ACCEPTED: change ${
|
|
3103
|
-
eventQueue.push("change",
|
|
3311
|
+
watcher.on("change", (path31) => {
|
|
3312
|
+
console.error(`[flywheel] RAW EVENT: change ${path31}`);
|
|
3313
|
+
if (shouldWatch(path31, vaultPath2)) {
|
|
3314
|
+
console.error(`[flywheel] ACCEPTED: change ${path31}`);
|
|
3315
|
+
eventQueue.push("change", path31);
|
|
3104
3316
|
} else {
|
|
3105
|
-
console.error(`[flywheel] FILTERED: change ${
|
|
3317
|
+
console.error(`[flywheel] FILTERED: change ${path31}`);
|
|
3106
3318
|
}
|
|
3107
3319
|
});
|
|
3108
|
-
watcher.on("unlink", (
|
|
3109
|
-
console.error(`[flywheel] RAW EVENT: unlink ${
|
|
3110
|
-
if (shouldWatch(
|
|
3111
|
-
console.error(`[flywheel] ACCEPTED: unlink ${
|
|
3112
|
-
eventQueue.push("unlink",
|
|
3320
|
+
watcher.on("unlink", (path31) => {
|
|
3321
|
+
console.error(`[flywheel] RAW EVENT: unlink ${path31}`);
|
|
3322
|
+
if (shouldWatch(path31, vaultPath2)) {
|
|
3323
|
+
console.error(`[flywheel] ACCEPTED: unlink ${path31}`);
|
|
3324
|
+
eventQueue.push("unlink", path31);
|
|
3113
3325
|
} else {
|
|
3114
|
-
console.error(`[flywheel] FILTERED: unlink ${
|
|
3326
|
+
console.error(`[flywheel] FILTERED: unlink ${path31}`);
|
|
3115
3327
|
}
|
|
3116
3328
|
});
|
|
3117
3329
|
watcher.on("ready", () => {
|
|
@@ -3241,9 +3453,14 @@ var FEEDBACK_BOOST_TIERS = [
|
|
|
3241
3453
|
{ minAccuracy: 0, minSamples: 5, boost: -4 }
|
|
3242
3454
|
];
|
|
3243
3455
|
function recordFeedback(stateDb2, entity, context, notePath, correct) {
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3456
|
+
try {
|
|
3457
|
+
stateDb2.db.prepare(
|
|
3458
|
+
"INSERT INTO wikilink_feedback (entity, context, note_path, correct) VALUES (?, ?, ?, ?)"
|
|
3459
|
+
).run(entity, context, notePath, correct ? 1 : 0);
|
|
3460
|
+
} catch (e) {
|
|
3461
|
+
console.error(`[Flywheel] recordFeedback failed for entity="${entity}": ${e}`);
|
|
3462
|
+
throw e;
|
|
3463
|
+
}
|
|
3247
3464
|
}
|
|
3248
3465
|
function getFeedback(stateDb2, entity, limit = 20) {
|
|
3249
3466
|
let rows;
|
|
@@ -3554,10 +3771,163 @@ function getDashboardData(stateDb2) {
|
|
|
3554
3771
|
}))
|
|
3555
3772
|
};
|
|
3556
3773
|
}
|
|
3774
|
+
function getEntityScoreTimeline(stateDb2, entityName, daysBack = 30, limit = 100) {
|
|
3775
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
3776
|
+
const rows = stateDb2.db.prepare(`
|
|
3777
|
+
SELECT timestamp, total_score, breakdown_json, note_path, passed, threshold
|
|
3778
|
+
FROM suggestion_events
|
|
3779
|
+
WHERE entity = ? AND timestamp >= ?
|
|
3780
|
+
ORDER BY timestamp ASC
|
|
3781
|
+
LIMIT ?
|
|
3782
|
+
`).all(entityName, cutoff, limit);
|
|
3783
|
+
return rows.map((r) => ({
|
|
3784
|
+
timestamp: r.timestamp,
|
|
3785
|
+
score: r.total_score,
|
|
3786
|
+
breakdown: JSON.parse(r.breakdown_json),
|
|
3787
|
+
notePath: r.note_path,
|
|
3788
|
+
passed: r.passed === 1,
|
|
3789
|
+
threshold: r.threshold
|
|
3790
|
+
}));
|
|
3791
|
+
}
|
|
3792
|
+
function getLayerContributionTimeseries(stateDb2, granularity = "day", daysBack = 30) {
|
|
3793
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
3794
|
+
const rows = stateDb2.db.prepare(`
|
|
3795
|
+
SELECT timestamp, breakdown_json
|
|
3796
|
+
FROM suggestion_events
|
|
3797
|
+
WHERE timestamp >= ?
|
|
3798
|
+
ORDER BY timestamp ASC
|
|
3799
|
+
`).all(cutoff);
|
|
3800
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3801
|
+
for (const row of rows) {
|
|
3802
|
+
const date = new Date(row.timestamp);
|
|
3803
|
+
let bucket;
|
|
3804
|
+
if (granularity === "week") {
|
|
3805
|
+
const jan4 = new Date(date.getFullYear(), 0, 4);
|
|
3806
|
+
const weekNum = Math.ceil(((date.getTime() - jan4.getTime()) / 864e5 + jan4.getDay() + 1) / 7);
|
|
3807
|
+
bucket = `${date.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
|
|
3808
|
+
} else {
|
|
3809
|
+
bucket = date.toISOString().slice(0, 10);
|
|
3810
|
+
}
|
|
3811
|
+
if (!buckets.has(bucket)) {
|
|
3812
|
+
buckets.set(bucket, { count: 0, layers: {} });
|
|
3813
|
+
}
|
|
3814
|
+
const acc = buckets.get(bucket);
|
|
3815
|
+
acc.count++;
|
|
3816
|
+
const breakdown = JSON.parse(row.breakdown_json);
|
|
3817
|
+
const layerMap = {
|
|
3818
|
+
contentMatch: breakdown.contentMatch,
|
|
3819
|
+
cooccurrenceBoost: breakdown.cooccurrenceBoost,
|
|
3820
|
+
typeBoost: breakdown.typeBoost,
|
|
3821
|
+
contextBoost: breakdown.contextBoost,
|
|
3822
|
+
recencyBoost: breakdown.recencyBoost,
|
|
3823
|
+
crossFolderBoost: breakdown.crossFolderBoost,
|
|
3824
|
+
hubBoost: breakdown.hubBoost,
|
|
3825
|
+
feedbackAdjustment: breakdown.feedbackAdjustment
|
|
3826
|
+
};
|
|
3827
|
+
if (breakdown.semanticBoost !== void 0) {
|
|
3828
|
+
layerMap.semanticBoost = breakdown.semanticBoost;
|
|
3829
|
+
}
|
|
3830
|
+
for (const [layer, value] of Object.entries(layerMap)) {
|
|
3831
|
+
acc.layers[layer] = (acc.layers[layer] ?? 0) + value;
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
const result = [];
|
|
3835
|
+
for (const [bucket, acc] of buckets) {
|
|
3836
|
+
const avgLayers = {};
|
|
3837
|
+
for (const [layer, sum] of Object.entries(acc.layers)) {
|
|
3838
|
+
avgLayers[layer] = Math.round(sum / acc.count * 1e3) / 1e3;
|
|
3839
|
+
}
|
|
3840
|
+
result.push({ bucket, layers: avgLayers });
|
|
3841
|
+
}
|
|
3842
|
+
return result;
|
|
3843
|
+
}
|
|
3844
|
+
function getExtendedDashboardData(stateDb2) {
|
|
3845
|
+
const base = getDashboardData(stateDb2);
|
|
3846
|
+
const recentCutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
3847
|
+
const eventRows = stateDb2.db.prepare(`
|
|
3848
|
+
SELECT breakdown_json FROM suggestion_events WHERE timestamp >= ?
|
|
3849
|
+
`).all(recentCutoff);
|
|
3850
|
+
const layerSums = {};
|
|
3851
|
+
const LAYER_NAMES = [
|
|
3852
|
+
"contentMatch",
|
|
3853
|
+
"cooccurrenceBoost",
|
|
3854
|
+
"typeBoost",
|
|
3855
|
+
"contextBoost",
|
|
3856
|
+
"recencyBoost",
|
|
3857
|
+
"crossFolderBoost",
|
|
3858
|
+
"hubBoost",
|
|
3859
|
+
"feedbackAdjustment",
|
|
3860
|
+
"semanticBoost"
|
|
3861
|
+
];
|
|
3862
|
+
for (const name of LAYER_NAMES) {
|
|
3863
|
+
layerSums[name] = { sum: 0, count: 0 };
|
|
3864
|
+
}
|
|
3865
|
+
for (const row of eventRows) {
|
|
3866
|
+
const breakdown = JSON.parse(row.breakdown_json);
|
|
3867
|
+
for (const name of LAYER_NAMES) {
|
|
3868
|
+
const val = breakdown[name];
|
|
3869
|
+
if (val !== void 0) {
|
|
3870
|
+
layerSums[name].sum += Math.abs(val);
|
|
3871
|
+
layerSums[name].count++;
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
const layerHealth = LAYER_NAMES.map((layer) => {
|
|
3876
|
+
const s = layerSums[layer];
|
|
3877
|
+
const avg = s.count > 0 ? Math.round(s.sum / s.count * 1e3) / 1e3 : 0;
|
|
3878
|
+
let status;
|
|
3879
|
+
if (s.count === 0) status = "zero-data";
|
|
3880
|
+
else if (avg > 0) status = "contributing";
|
|
3881
|
+
else status = "dormant";
|
|
3882
|
+
return { layer, status, avgContribution: avg, eventCount: s.count };
|
|
3883
|
+
});
|
|
3884
|
+
const topEntityRows = stateDb2.db.prepare(`
|
|
3885
|
+
SELECT entity, COUNT(*) as cnt, AVG(total_score) as avg_score,
|
|
3886
|
+
SUM(CASE WHEN passed = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as pass_rate
|
|
3887
|
+
FROM suggestion_events
|
|
3888
|
+
GROUP BY entity
|
|
3889
|
+
ORDER BY cnt DESC
|
|
3890
|
+
LIMIT 10
|
|
3891
|
+
`).all();
|
|
3892
|
+
const topEntities = topEntityRows.map((r) => ({
|
|
3893
|
+
entity: r.entity,
|
|
3894
|
+
suggestionCount: r.cnt,
|
|
3895
|
+
avgScore: Math.round(r.avg_score * 100) / 100,
|
|
3896
|
+
passRate: Math.round(r.pass_rate * 1e3) / 1e3
|
|
3897
|
+
}));
|
|
3898
|
+
const feedbackTrendRows = stateDb2.db.prepare(`
|
|
3899
|
+
SELECT date(created_at) as day, COUNT(*) as count
|
|
3900
|
+
FROM wikilink_feedback
|
|
3901
|
+
WHERE created_at >= datetime('now', '-30 days')
|
|
3902
|
+
GROUP BY day
|
|
3903
|
+
ORDER BY day
|
|
3904
|
+
`).all();
|
|
3905
|
+
const feedbackTrend = feedbackTrendRows.map((r) => ({
|
|
3906
|
+
day: r.day,
|
|
3907
|
+
count: r.count
|
|
3908
|
+
}));
|
|
3909
|
+
const suppressionRows = stateDb2.db.prepare(`
|
|
3910
|
+
SELECT entity, false_positive_rate, updated_at
|
|
3911
|
+
FROM wikilink_suppressions
|
|
3912
|
+
ORDER BY updated_at DESC
|
|
3913
|
+
`).all();
|
|
3914
|
+
const suppressionChanges = suppressionRows.map((r) => ({
|
|
3915
|
+
entity: r.entity,
|
|
3916
|
+
falsePositiveRate: r.false_positive_rate,
|
|
3917
|
+
updatedAt: r.updated_at
|
|
3918
|
+
}));
|
|
3919
|
+
return {
|
|
3920
|
+
...base,
|
|
3921
|
+
layerHealth,
|
|
3922
|
+
topEntities,
|
|
3923
|
+
feedbackTrend,
|
|
3924
|
+
suppressionChanges
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3557
3927
|
|
|
3558
3928
|
// src/core/write/git.ts
|
|
3559
3929
|
import { simpleGit, CheckRepoActions } from "simple-git";
|
|
3560
|
-
import
|
|
3930
|
+
import path7 from "path";
|
|
3561
3931
|
import fs6 from "fs/promises";
|
|
3562
3932
|
import {
|
|
3563
3933
|
setWriteState,
|
|
@@ -3611,7 +3981,7 @@ function clearLastMutationCommit() {
|
|
|
3611
3981
|
}
|
|
3612
3982
|
}
|
|
3613
3983
|
async function checkGitLock(vaultPath2) {
|
|
3614
|
-
const lockPath =
|
|
3984
|
+
const lockPath = path7.join(vaultPath2, ".git/index.lock");
|
|
3615
3985
|
try {
|
|
3616
3986
|
const stat3 = await fs6.stat(lockPath);
|
|
3617
3987
|
const ageMs = Date.now() - stat3.mtimeMs;
|
|
@@ -3634,7 +4004,7 @@ async function isGitRepo(vaultPath2) {
|
|
|
3634
4004
|
}
|
|
3635
4005
|
}
|
|
3636
4006
|
async function checkLockFile(vaultPath2) {
|
|
3637
|
-
const lockPath =
|
|
4007
|
+
const lockPath = path7.join(vaultPath2, ".git/index.lock");
|
|
3638
4008
|
try {
|
|
3639
4009
|
const stat3 = await fs6.stat(lockPath);
|
|
3640
4010
|
const ageMs = Date.now() - stat3.mtimeMs;
|
|
@@ -3684,7 +4054,7 @@ async function commitChange(vaultPath2, filePath, messagePrefix, retryConfig = D
|
|
|
3684
4054
|
}
|
|
3685
4055
|
}
|
|
3686
4056
|
await git.add(filePath);
|
|
3687
|
-
const fileName =
|
|
4057
|
+
const fileName = path7.basename(filePath);
|
|
3688
4058
|
const commitMessage = `${messagePrefix} Update ${fileName}`;
|
|
3689
4059
|
const result = await git.commit(commitMessage);
|
|
3690
4060
|
if (result.commit) {
|
|
@@ -3878,7 +4248,7 @@ function setHintsStateDb(stateDb2) {
|
|
|
3878
4248
|
|
|
3879
4249
|
// src/core/shared/recency.ts
|
|
3880
4250
|
import { readdir, readFile, stat } from "fs/promises";
|
|
3881
|
-
import
|
|
4251
|
+
import path8 from "path";
|
|
3882
4252
|
import {
|
|
3883
4253
|
getEntityName,
|
|
3884
4254
|
recordEntityMention,
|
|
@@ -3900,9 +4270,9 @@ async function* walkMarkdownFiles(dir, baseDir) {
|
|
|
3900
4270
|
try {
|
|
3901
4271
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
3902
4272
|
for (const entry of entries) {
|
|
3903
|
-
const fullPath =
|
|
3904
|
-
const relativePath =
|
|
3905
|
-
const topFolder = relativePath.split(
|
|
4273
|
+
const fullPath = path8.join(dir, entry.name);
|
|
4274
|
+
const relativePath = path8.relative(baseDir, fullPath);
|
|
4275
|
+
const topFolder = relativePath.split(path8.sep)[0];
|
|
3906
4276
|
if (EXCLUDED_FOLDERS.has(topFolder)) {
|
|
3907
4277
|
continue;
|
|
3908
4278
|
}
|
|
@@ -3986,14 +4356,16 @@ function loadRecencyFromStateDb() {
|
|
|
3986
4356
|
}
|
|
3987
4357
|
function saveRecencyToStateDb(index) {
|
|
3988
4358
|
if (!moduleStateDb3) {
|
|
3989
|
-
console.error("[Flywheel] No StateDb available
|
|
4359
|
+
console.error("[Flywheel] saveRecencyToStateDb: No StateDb available (moduleStateDb is null)");
|
|
3990
4360
|
return;
|
|
3991
4361
|
}
|
|
4362
|
+
console.error(`[Flywheel] saveRecencyToStateDb: Saving ${index.lastMentioned.size} entries...`);
|
|
3992
4363
|
try {
|
|
3993
4364
|
for (const [entityNameLower, timestamp] of index.lastMentioned) {
|
|
3994
4365
|
recordEntityMention(moduleStateDb3, entityNameLower, new Date(timestamp));
|
|
3995
4366
|
}
|
|
3996
|
-
|
|
4367
|
+
const count = moduleStateDb3.db.prepare("SELECT COUNT(*) as cnt FROM recency").get();
|
|
4368
|
+
console.error(`[Flywheel] Saved recency: ${index.lastMentioned.size} entries \u2192 ${count.cnt} rows in table`);
|
|
3997
4369
|
} catch (e) {
|
|
3998
4370
|
console.error("[Flywheel] Failed to save recency to StateDb:", e);
|
|
3999
4371
|
}
|
|
@@ -4780,7 +5152,7 @@ function tokenize(text) {
|
|
|
4780
5152
|
|
|
4781
5153
|
// src/core/shared/cooccurrence.ts
|
|
4782
5154
|
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
4783
|
-
import
|
|
5155
|
+
import path9 from "path";
|
|
4784
5156
|
var DEFAULT_MIN_COOCCURRENCE = 2;
|
|
4785
5157
|
var EXCLUDED_FOLDERS2 = /* @__PURE__ */ new Set([
|
|
4786
5158
|
"templates",
|
|
@@ -4814,9 +5186,9 @@ async function* walkMarkdownFiles2(dir, baseDir) {
|
|
|
4814
5186
|
try {
|
|
4815
5187
|
const entries = await readdir2(dir, { withFileTypes: true });
|
|
4816
5188
|
for (const entry of entries) {
|
|
4817
|
-
const fullPath =
|
|
4818
|
-
const relativePath =
|
|
4819
|
-
const topFolder = relativePath.split(
|
|
5189
|
+
const fullPath = path9.join(dir, entry.name);
|
|
5190
|
+
const relativePath = path9.relative(baseDir, fullPath);
|
|
5191
|
+
const topFolder = relativePath.split(path9.sep)[0];
|
|
4820
5192
|
if (EXCLUDED_FOLDERS2.has(topFolder)) {
|
|
4821
5193
|
continue;
|
|
4822
5194
|
}
|
|
@@ -5079,12 +5451,15 @@ function processWikilinks(content, notePath) {
|
|
|
5079
5451
|
const resolved = resolveAliasWikilinks(content, sortedEntities, {
|
|
5080
5452
|
caseInsensitive: true
|
|
5081
5453
|
});
|
|
5454
|
+
const step1LinkedEntities = new Set(resolved.linkedEntities.map((e) => e.toLowerCase()));
|
|
5082
5455
|
const result = applyWikilinks(resolved.content, sortedEntities, {
|
|
5083
5456
|
firstOccurrenceOnly: true,
|
|
5084
|
-
caseInsensitive: true
|
|
5457
|
+
caseInsensitive: true,
|
|
5458
|
+
alreadyLinked: step1LinkedEntities
|
|
5085
5459
|
});
|
|
5086
5460
|
const implicitEnabled = moduleConfig?.implicit_detection !== false;
|
|
5087
|
-
const
|
|
5461
|
+
const validPatterns = new Set(ALL_IMPLICIT_PATTERNS);
|
|
5462
|
+
const implicitPatterns = moduleConfig?.implicit_patterns?.length ? moduleConfig.implicit_patterns.filter((p) => validPatterns.has(p)) : [...ALL_IMPLICIT_PATTERNS];
|
|
5088
5463
|
const implicitMatches = detectImplicitEntities(result.content, {
|
|
5089
5464
|
detectImplicit: implicitEnabled,
|
|
5090
5465
|
implicitPatterns,
|
|
@@ -5505,8 +5880,10 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
5505
5880
|
excludeLinked = true,
|
|
5506
5881
|
strictness = getEffectiveStrictness(options.notePath),
|
|
5507
5882
|
notePath,
|
|
5508
|
-
detail = false
|
|
5883
|
+
detail = false,
|
|
5884
|
+
disabledLayers = []
|
|
5509
5885
|
} = options;
|
|
5886
|
+
const disabled = new Set(disabledLayers);
|
|
5510
5887
|
const config = STRICTNESS_CONFIGS[strictness];
|
|
5511
5888
|
const adaptiveMinScore = getAdaptiveMinScore(content.length, config.minSuggestionScore);
|
|
5512
5889
|
const noteContext = notePath ? getNoteContext(notePath) : "general";
|
|
@@ -5547,31 +5924,31 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
5547
5924
|
for (const { entity, category } of entitiesWithTypes) {
|
|
5548
5925
|
const entityName = entity.name;
|
|
5549
5926
|
if (!entityName) continue;
|
|
5550
|
-
if (entityName.length > MAX_ENTITY_LENGTH) {
|
|
5927
|
+
if (!disabled.has("length_filter") && entityName.length > MAX_ENTITY_LENGTH) {
|
|
5551
5928
|
continue;
|
|
5552
5929
|
}
|
|
5553
|
-
if (isLikelyArticleTitle(entityName)) {
|
|
5930
|
+
if (!disabled.has("article_filter") && isLikelyArticleTitle(entityName)) {
|
|
5554
5931
|
continue;
|
|
5555
5932
|
}
|
|
5556
5933
|
if (linkedEntities.has(entityName.toLowerCase())) {
|
|
5557
5934
|
continue;
|
|
5558
5935
|
}
|
|
5559
|
-
const contentScore = scoreEntity(entity, contentTokens, contentStems, config);
|
|
5936
|
+
const contentScore = disabled.has("exact_match") && disabled.has("stem_match") ? 0 : scoreEntity(entity, contentTokens, contentStems, config);
|
|
5560
5937
|
let score = contentScore;
|
|
5561
5938
|
if (contentScore > 0) {
|
|
5562
5939
|
entitiesWithContentMatch.add(entityName);
|
|
5563
5940
|
}
|
|
5564
|
-
const layerTypeBoost = TYPE_BOOST[category] || 0;
|
|
5941
|
+
const layerTypeBoost = disabled.has("type_boost") ? 0 : TYPE_BOOST[category] || 0;
|
|
5565
5942
|
score += layerTypeBoost;
|
|
5566
|
-
const layerContextBoost = contextBoosts[category] || 0;
|
|
5943
|
+
const layerContextBoost = disabled.has("context_boost") ? 0 : contextBoosts[category] || 0;
|
|
5567
5944
|
score += layerContextBoost;
|
|
5568
|
-
const layerRecencyBoost = recencyIndex ? getRecencyBoost(entityName, recencyIndex) : 0;
|
|
5945
|
+
const layerRecencyBoost = disabled.has("recency") ? 0 : recencyIndex ? getRecencyBoost(entityName, recencyIndex) : 0;
|
|
5569
5946
|
score += layerRecencyBoost;
|
|
5570
|
-
const layerCrossFolderBoost = notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
5947
|
+
const layerCrossFolderBoost = disabled.has("cross_folder") ? 0 : notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
5571
5948
|
score += layerCrossFolderBoost;
|
|
5572
|
-
const layerHubBoost = getHubBoost(entity);
|
|
5949
|
+
const layerHubBoost = disabled.has("hub_boost") ? 0 : getHubBoost(entity);
|
|
5573
5950
|
score += layerHubBoost;
|
|
5574
|
-
const layerFeedbackAdj = feedbackBoosts.get(entityName) ?? 0;
|
|
5951
|
+
const layerFeedbackAdj = disabled.has("feedback") ? 0 : feedbackBoosts.get(entityName) ?? 0;
|
|
5575
5952
|
score += layerFeedbackAdj;
|
|
5576
5953
|
if (score > 0) {
|
|
5577
5954
|
directlyMatchedEntities.add(entityName);
|
|
@@ -5595,12 +5972,12 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
5595
5972
|
});
|
|
5596
5973
|
}
|
|
5597
5974
|
}
|
|
5598
|
-
if (cooccurrenceIndex && directlyMatchedEntities.size > 0) {
|
|
5975
|
+
if (!disabled.has("cooccurrence") && cooccurrenceIndex && directlyMatchedEntities.size > 0) {
|
|
5599
5976
|
for (const { entity, category } of entitiesWithTypes) {
|
|
5600
5977
|
const entityName = entity.name;
|
|
5601
5978
|
if (!entityName) continue;
|
|
5602
|
-
if (entityName.length > MAX_ENTITY_LENGTH) continue;
|
|
5603
|
-
if (isLikelyArticleTitle(entityName)) continue;
|
|
5979
|
+
if (!disabled.has("length_filter") && entityName.length > MAX_ENTITY_LENGTH) continue;
|
|
5980
|
+
if (!disabled.has("article_filter") && isLikelyArticleTitle(entityName)) continue;
|
|
5604
5981
|
if (linkedEntities.has(entityName.toLowerCase())) continue;
|
|
5605
5982
|
const boost = getCooccurrenceBoost(entityName, directlyMatchedEntities, cooccurrenceIndex, recencyIndex);
|
|
5606
5983
|
if (boost > 0) {
|
|
@@ -5617,12 +5994,12 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
5617
5994
|
continue;
|
|
5618
5995
|
}
|
|
5619
5996
|
entitiesWithContentMatch.add(entityName);
|
|
5620
|
-
const typeBoost = TYPE_BOOST[category] || 0;
|
|
5621
|
-
const contextBoost = contextBoosts[category] || 0;
|
|
5622
|
-
const recencyBoostVal = recencyIndex ? getRecencyBoost(entityName, recencyIndex) : 0;
|
|
5623
|
-
const crossFolderBoost = notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
5624
|
-
const hubBoost = getHubBoost(entity);
|
|
5625
|
-
const feedbackAdj = feedbackBoosts.get(entityName) ?? 0;
|
|
5997
|
+
const typeBoost = disabled.has("type_boost") ? 0 : TYPE_BOOST[category] || 0;
|
|
5998
|
+
const contextBoost = disabled.has("context_boost") ? 0 : contextBoosts[category] || 0;
|
|
5999
|
+
const recencyBoostVal = disabled.has("recency") ? 0 : recencyIndex ? getRecencyBoost(entityName, recencyIndex) : 0;
|
|
6000
|
+
const crossFolderBoost = disabled.has("cross_folder") ? 0 : notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
6001
|
+
const hubBoost = disabled.has("hub_boost") ? 0 : getHubBoost(entity);
|
|
6002
|
+
const feedbackAdj = disabled.has("feedback") ? 0 : feedbackBoosts.get(entityName) ?? 0;
|
|
5626
6003
|
const totalBoost = boost + typeBoost + contextBoost + recencyBoostVal + crossFolderBoost + hubBoost + feedbackAdj;
|
|
5627
6004
|
if (totalBoost >= adaptiveMinScore) {
|
|
5628
6005
|
scoredEntities.push({
|
|
@@ -5646,7 +6023,7 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
5646
6023
|
}
|
|
5647
6024
|
}
|
|
5648
6025
|
}
|
|
5649
|
-
if (content.length >= 20 && hasEntityEmbeddingsIndex()) {
|
|
6026
|
+
if (!disabled.has("semantic") && content.length >= 20 && hasEntityEmbeddingsIndex()) {
|
|
5650
6027
|
try {
|
|
5651
6028
|
const contentEmbedding = await embedTextCached(content);
|
|
5652
6029
|
const alreadyScoredNames = new Set(scoredEntities.map((e) => e.name));
|
|
@@ -5668,14 +6045,14 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
5668
6045
|
(et) => et.entity.name === match.entityName
|
|
5669
6046
|
);
|
|
5670
6047
|
if (!entityWithType) continue;
|
|
5671
|
-
if (match.entityName.length > MAX_ENTITY_LENGTH) continue;
|
|
5672
|
-
if (isLikelyArticleTitle(match.entityName)) continue;
|
|
6048
|
+
if (!disabled.has("length_filter") && match.entityName.length > MAX_ENTITY_LENGTH) continue;
|
|
6049
|
+
if (!disabled.has("article_filter") && isLikelyArticleTitle(match.entityName)) continue;
|
|
5673
6050
|
const { entity, category } = entityWithType;
|
|
5674
|
-
const layerTypeBoost = TYPE_BOOST[category] || 0;
|
|
5675
|
-
const layerContextBoost = contextBoosts[category] || 0;
|
|
5676
|
-
const layerHubBoost = getHubBoost(entity);
|
|
5677
|
-
const layerCrossFolderBoost = notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
5678
|
-
const layerFeedbackAdj = feedbackBoosts.get(match.entityName) ?? 0;
|
|
6051
|
+
const layerTypeBoost = disabled.has("type_boost") ? 0 : TYPE_BOOST[category] || 0;
|
|
6052
|
+
const layerContextBoost = disabled.has("context_boost") ? 0 : contextBoosts[category] || 0;
|
|
6053
|
+
const layerHubBoost = disabled.has("hub_boost") ? 0 : getHubBoost(entity);
|
|
6054
|
+
const layerCrossFolderBoost = disabled.has("cross_folder") ? 0 : notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
6055
|
+
const layerFeedbackAdj = disabled.has("feedback") ? 0 : feedbackBoosts.get(match.entityName) ?? 0;
|
|
5679
6056
|
const totalScore = boost + layerTypeBoost + layerContextBoost + layerHubBoost + layerCrossFolderBoost + layerFeedbackAdj;
|
|
5680
6057
|
if (totalScore >= adaptiveMinScore) {
|
|
5681
6058
|
scoredEntities.push({
|
|
@@ -5717,7 +6094,51 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
5717
6094
|
}
|
|
5718
6095
|
return 0;
|
|
5719
6096
|
});
|
|
5720
|
-
|
|
6097
|
+
if (moduleStateDb4 && notePath) {
|
|
6098
|
+
try {
|
|
6099
|
+
const now = Date.now();
|
|
6100
|
+
const insertStmt = moduleStateDb4.db.prepare(`
|
|
6101
|
+
INSERT OR IGNORE INTO suggestion_events
|
|
6102
|
+
(timestamp, note_path, entity, total_score, breakdown_json, threshold, passed, strictness, applied, pipeline_event_id)
|
|
6103
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL)
|
|
6104
|
+
`);
|
|
6105
|
+
const persistTransaction = moduleStateDb4.db.transaction(() => {
|
|
6106
|
+
for (const e of relevantEntities) {
|
|
6107
|
+
insertStmt.run(
|
|
6108
|
+
now,
|
|
6109
|
+
notePath,
|
|
6110
|
+
e.name,
|
|
6111
|
+
e.score,
|
|
6112
|
+
JSON.stringify(e.breakdown),
|
|
6113
|
+
adaptiveMinScore,
|
|
6114
|
+
1,
|
|
6115
|
+
// passed threshold (these are relevantEntities)
|
|
6116
|
+
strictness
|
|
6117
|
+
);
|
|
6118
|
+
}
|
|
6119
|
+
for (const e of scoredEntities) {
|
|
6120
|
+
if (!entitiesWithContentMatch.has(e.name)) continue;
|
|
6121
|
+
if (relevantEntities.some((r) => r.name === e.name)) continue;
|
|
6122
|
+
insertStmt.run(
|
|
6123
|
+
now,
|
|
6124
|
+
notePath,
|
|
6125
|
+
e.name,
|
|
6126
|
+
e.score,
|
|
6127
|
+
JSON.stringify(e.breakdown),
|
|
6128
|
+
adaptiveMinScore,
|
|
6129
|
+
0,
|
|
6130
|
+
// did not pass threshold
|
|
6131
|
+
strictness
|
|
6132
|
+
);
|
|
6133
|
+
}
|
|
6134
|
+
});
|
|
6135
|
+
persistTransaction();
|
|
6136
|
+
} catch {
|
|
6137
|
+
}
|
|
6138
|
+
}
|
|
6139
|
+
const currentNoteStem = notePath ? notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() : null;
|
|
6140
|
+
const filtered = currentNoteStem ? relevantEntities.filter((e) => e.name.toLowerCase() !== currentNoteStem) : relevantEntities;
|
|
6141
|
+
const topEntries = filtered.slice(0, maxSuggestions);
|
|
5721
6142
|
const topSuggestions = topEntries.map((e) => e.name);
|
|
5722
6143
|
if (topSuggestions.length === 0) {
|
|
5723
6144
|
return emptyResult;
|
|
@@ -6081,11 +6502,11 @@ function countFTS5Mentions(term) {
|
|
|
6081
6502
|
}
|
|
6082
6503
|
|
|
6083
6504
|
// src/core/read/taskCache.ts
|
|
6084
|
-
import * as
|
|
6505
|
+
import * as path11 from "path";
|
|
6085
6506
|
|
|
6086
6507
|
// src/tools/read/tasks.ts
|
|
6087
6508
|
import * as fs8 from "fs";
|
|
6088
|
-
import * as
|
|
6509
|
+
import * as path10 from "path";
|
|
6089
6510
|
var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
|
|
6090
6511
|
var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
|
|
6091
6512
|
var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
|
|
@@ -6154,7 +6575,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
|
|
|
6154
6575
|
const allTasks = [];
|
|
6155
6576
|
for (const note of index.notes.values()) {
|
|
6156
6577
|
if (folder && !note.path.startsWith(folder)) continue;
|
|
6157
|
-
const absolutePath =
|
|
6578
|
+
const absolutePath = path10.join(vaultPath2, note.path);
|
|
6158
6579
|
const tasks = await extractTasksFromNote(note.path, absolutePath);
|
|
6159
6580
|
allTasks.push(...tasks);
|
|
6160
6581
|
}
|
|
@@ -6198,7 +6619,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
|
|
|
6198
6619
|
async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
|
|
6199
6620
|
const note = index.notes.get(notePath);
|
|
6200
6621
|
if (!note) return null;
|
|
6201
|
-
const absolutePath =
|
|
6622
|
+
const absolutePath = path10.join(vaultPath2, notePath);
|
|
6202
6623
|
let tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6203
6624
|
if (excludeTags.length > 0) {
|
|
6204
6625
|
tasks = tasks.filter(
|
|
@@ -6290,7 +6711,7 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
6290
6711
|
}
|
|
6291
6712
|
const allRows = [];
|
|
6292
6713
|
for (const notePath of notePaths) {
|
|
6293
|
-
const absolutePath =
|
|
6714
|
+
const absolutePath = path11.join(vaultPath2, notePath);
|
|
6294
6715
|
const tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6295
6716
|
for (const task of tasks) {
|
|
6296
6717
|
if (excludeTags?.length && excludeTags.some((t) => task.tags.includes(t))) {
|
|
@@ -6332,7 +6753,7 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
6332
6753
|
async function updateTaskCacheForFile(vaultPath2, relativePath) {
|
|
6333
6754
|
if (!db3) return;
|
|
6334
6755
|
db3.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
|
|
6335
|
-
const absolutePath =
|
|
6756
|
+
const absolutePath = path11.join(vaultPath2, relativePath);
|
|
6336
6757
|
const tasks = await extractTasksFromNote(relativePath, absolutePath);
|
|
6337
6758
|
if (tasks.length > 0) {
|
|
6338
6759
|
const insertStmt = db3.prepare(`
|
|
@@ -6472,7 +6893,7 @@ import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, get
|
|
|
6472
6893
|
|
|
6473
6894
|
// src/tools/read/graph.ts
|
|
6474
6895
|
import * as fs9 from "fs";
|
|
6475
|
-
import * as
|
|
6896
|
+
import * as path12 from "path";
|
|
6476
6897
|
import { z } from "zod";
|
|
6477
6898
|
|
|
6478
6899
|
// src/core/read/constants.ts
|
|
@@ -6756,7 +7177,7 @@ function requireIndex() {
|
|
|
6756
7177
|
// src/tools/read/graph.ts
|
|
6757
7178
|
async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
|
|
6758
7179
|
try {
|
|
6759
|
-
const fullPath =
|
|
7180
|
+
const fullPath = path12.join(vaultPath2, sourcePath);
|
|
6760
7181
|
const content = await fs9.promises.readFile(fullPath, "utf-8");
|
|
6761
7182
|
const allLines = content.split("\n");
|
|
6762
7183
|
let fmLines = 0;
|
|
@@ -7070,14 +7491,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
7070
7491
|
};
|
|
7071
7492
|
function findSimilarEntity2(target, entities) {
|
|
7072
7493
|
const targetLower = target.toLowerCase();
|
|
7073
|
-
for (const [name,
|
|
7494
|
+
for (const [name, path31] of entities) {
|
|
7074
7495
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
7075
|
-
return
|
|
7496
|
+
return path31;
|
|
7076
7497
|
}
|
|
7077
7498
|
}
|
|
7078
|
-
for (const [name,
|
|
7499
|
+
for (const [name, path31] of entities) {
|
|
7079
7500
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
7080
|
-
return
|
|
7501
|
+
return path31;
|
|
7081
7502
|
}
|
|
7082
7503
|
}
|
|
7083
7504
|
return void 0;
|
|
@@ -8054,8 +8475,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8054
8475
|
daily_counts: z3.record(z3.number())
|
|
8055
8476
|
}).describe("Activity summary for the last 7 days")
|
|
8056
8477
|
};
|
|
8057
|
-
function isPeriodicNote2(
|
|
8058
|
-
const filename =
|
|
8478
|
+
function isPeriodicNote2(path31) {
|
|
8479
|
+
const filename = path31.split("/").pop() || "";
|
|
8059
8480
|
const nameWithoutExt = filename.replace(/\.md$/, "");
|
|
8060
8481
|
const patterns = [
|
|
8061
8482
|
/^\d{4}-\d{2}-\d{2}$/,
|
|
@@ -8070,7 +8491,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
8070
8491
|
// YYYY (yearly)
|
|
8071
8492
|
];
|
|
8072
8493
|
const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
|
|
8073
|
-
const folder =
|
|
8494
|
+
const folder = path31.split("/")[0]?.toLowerCase() || "";
|
|
8074
8495
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
8075
8496
|
}
|
|
8076
8497
|
server2.registerTool(
|
|
@@ -8478,7 +8899,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
8478
8899
|
|
|
8479
8900
|
// src/tools/read/system.ts
|
|
8480
8901
|
import * as fs11 from "fs";
|
|
8481
|
-
import * as
|
|
8902
|
+
import * as path13 from "path";
|
|
8482
8903
|
import { z as z5 } from "zod";
|
|
8483
8904
|
import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
|
|
8484
8905
|
|
|
@@ -8778,7 +9199,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
8778
9199
|
continue;
|
|
8779
9200
|
}
|
|
8780
9201
|
try {
|
|
8781
|
-
const fullPath =
|
|
9202
|
+
const fullPath = path13.join(vaultPath2, note.path);
|
|
8782
9203
|
const content = await fs11.promises.readFile(fullPath, "utf-8");
|
|
8783
9204
|
const lines = content.split("\n");
|
|
8784
9205
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -8894,7 +9315,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
8894
9315
|
let wordCount;
|
|
8895
9316
|
if (include_word_count) {
|
|
8896
9317
|
try {
|
|
8897
|
-
const fullPath =
|
|
9318
|
+
const fullPath = path13.join(vaultPath2, resolvedPath);
|
|
8898
9319
|
const content = await fs11.promises.readFile(fullPath, "utf-8");
|
|
8899
9320
|
wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
8900
9321
|
} catch {
|
|
@@ -9125,7 +9546,7 @@ import { z as z6 } from "zod";
|
|
|
9125
9546
|
|
|
9126
9547
|
// src/tools/read/structure.ts
|
|
9127
9548
|
import * as fs12 from "fs";
|
|
9128
|
-
import * as
|
|
9549
|
+
import * as path14 from "path";
|
|
9129
9550
|
var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
|
|
9130
9551
|
function extractHeadings(content) {
|
|
9131
9552
|
const lines = content.split("\n");
|
|
@@ -9179,7 +9600,7 @@ function buildSections(headings, totalLines) {
|
|
|
9179
9600
|
async function getNoteStructure(index, notePath, vaultPath2) {
|
|
9180
9601
|
const note = index.notes.get(notePath);
|
|
9181
9602
|
if (!note) return null;
|
|
9182
|
-
const absolutePath =
|
|
9603
|
+
const absolutePath = path14.join(vaultPath2, notePath);
|
|
9183
9604
|
let content;
|
|
9184
9605
|
try {
|
|
9185
9606
|
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
@@ -9202,7 +9623,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
|
|
|
9202
9623
|
async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
|
|
9203
9624
|
const note = index.notes.get(notePath);
|
|
9204
9625
|
if (!note) return null;
|
|
9205
|
-
const absolutePath =
|
|
9626
|
+
const absolutePath = path14.join(vaultPath2, notePath);
|
|
9206
9627
|
let content;
|
|
9207
9628
|
try {
|
|
9208
9629
|
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
@@ -9244,7 +9665,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
9244
9665
|
const results = [];
|
|
9245
9666
|
for (const note of index.notes.values()) {
|
|
9246
9667
|
if (folder && !note.path.startsWith(folder)) continue;
|
|
9247
|
-
const absolutePath =
|
|
9668
|
+
const absolutePath = path14.join(vaultPath2, note.path);
|
|
9248
9669
|
let content;
|
|
9249
9670
|
try {
|
|
9250
9671
|
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
@@ -9278,18 +9699,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9278
9699
|
include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
|
|
9279
9700
|
}
|
|
9280
9701
|
},
|
|
9281
|
-
async ({ path:
|
|
9702
|
+
async ({ path: path31, include_content }) => {
|
|
9282
9703
|
const index = getIndex();
|
|
9283
9704
|
const vaultPath2 = getVaultPath();
|
|
9284
|
-
const result = await getNoteStructure(index,
|
|
9705
|
+
const result = await getNoteStructure(index, path31, vaultPath2);
|
|
9285
9706
|
if (!result) {
|
|
9286
9707
|
return {
|
|
9287
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
9708
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path31 }, null, 2) }]
|
|
9288
9709
|
};
|
|
9289
9710
|
}
|
|
9290
9711
|
if (include_content) {
|
|
9291
9712
|
for (const section of result.sections) {
|
|
9292
|
-
const sectionResult = await getSectionContent(index,
|
|
9713
|
+
const sectionResult = await getSectionContent(index, path31, section.heading.text, vaultPath2, true);
|
|
9293
9714
|
if (sectionResult) {
|
|
9294
9715
|
section.content = sectionResult.content;
|
|
9295
9716
|
}
|
|
@@ -9311,15 +9732,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9311
9732
|
include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
|
|
9312
9733
|
}
|
|
9313
9734
|
},
|
|
9314
|
-
async ({ path:
|
|
9735
|
+
async ({ path: path31, heading, include_subheadings }) => {
|
|
9315
9736
|
const index = getIndex();
|
|
9316
9737
|
const vaultPath2 = getVaultPath();
|
|
9317
|
-
const result = await getSectionContent(index,
|
|
9738
|
+
const result = await getSectionContent(index, path31, heading, vaultPath2, include_subheadings);
|
|
9318
9739
|
if (!result) {
|
|
9319
9740
|
return {
|
|
9320
9741
|
content: [{ type: "text", text: JSON.stringify({
|
|
9321
9742
|
error: "Section not found",
|
|
9322
|
-
path:
|
|
9743
|
+
path: path31,
|
|
9323
9744
|
heading
|
|
9324
9745
|
}, null, 2) }]
|
|
9325
9746
|
};
|
|
@@ -9373,16 +9794,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9373
9794
|
offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
9374
9795
|
}
|
|
9375
9796
|
},
|
|
9376
|
-
async ({ path:
|
|
9797
|
+
async ({ path: path31, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
|
|
9377
9798
|
const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
|
|
9378
9799
|
const index = getIndex();
|
|
9379
9800
|
const vaultPath2 = getVaultPath();
|
|
9380
9801
|
const config = getConfig();
|
|
9381
|
-
if (
|
|
9382
|
-
const result2 = await getTasksFromNote(index,
|
|
9802
|
+
if (path31) {
|
|
9803
|
+
const result2 = await getTasksFromNote(index, path31, vaultPath2, config.exclude_task_tags || []);
|
|
9383
9804
|
if (!result2) {
|
|
9384
9805
|
return {
|
|
9385
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
9806
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path31 }, null, 2) }]
|
|
9386
9807
|
};
|
|
9387
9808
|
}
|
|
9388
9809
|
let filtered = result2;
|
|
@@ -9392,7 +9813,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9392
9813
|
const paged2 = filtered.slice(offset, offset + limit);
|
|
9393
9814
|
return {
|
|
9394
9815
|
content: [{ type: "text", text: JSON.stringify({
|
|
9395
|
-
path:
|
|
9816
|
+
path: path31,
|
|
9396
9817
|
total_count: filtered.length,
|
|
9397
9818
|
returned_count: paged2.length,
|
|
9398
9819
|
open: result2.filter((t) => t.status === "open").length,
|
|
@@ -9548,7 +9969,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
9548
9969
|
// src/tools/read/migrations.ts
|
|
9549
9970
|
import { z as z7 } from "zod";
|
|
9550
9971
|
import * as fs13 from "fs/promises";
|
|
9551
|
-
import * as
|
|
9972
|
+
import * as path15 from "path";
|
|
9552
9973
|
import matter2 from "gray-matter";
|
|
9553
9974
|
function getNotesInFolder(index, folder) {
|
|
9554
9975
|
const notes = [];
|
|
@@ -9561,7 +9982,7 @@ function getNotesInFolder(index, folder) {
|
|
|
9561
9982
|
return notes;
|
|
9562
9983
|
}
|
|
9563
9984
|
async function readFileContent(notePath, vaultPath2) {
|
|
9564
|
-
const fullPath =
|
|
9985
|
+
const fullPath = path15.join(vaultPath2, notePath);
|
|
9565
9986
|
try {
|
|
9566
9987
|
return await fs13.readFile(fullPath, "utf-8");
|
|
9567
9988
|
} catch {
|
|
@@ -9569,7 +9990,7 @@ async function readFileContent(notePath, vaultPath2) {
|
|
|
9569
9990
|
}
|
|
9570
9991
|
}
|
|
9571
9992
|
async function writeFileContent(notePath, vaultPath2, content) {
|
|
9572
|
-
const fullPath =
|
|
9993
|
+
const fullPath = path15.join(vaultPath2, notePath);
|
|
9573
9994
|
try {
|
|
9574
9995
|
await fs13.writeFile(fullPath, content, "utf-8");
|
|
9575
9996
|
return true;
|
|
@@ -9750,7 +10171,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
|
|
|
9750
10171
|
|
|
9751
10172
|
// src/tools/read/graphAnalysis.ts
|
|
9752
10173
|
import fs14 from "node:fs";
|
|
9753
|
-
import
|
|
10174
|
+
import path16 from "node:path";
|
|
9754
10175
|
import { z as z8 } from "zod";
|
|
9755
10176
|
|
|
9756
10177
|
// src/tools/read/schema.ts
|
|
@@ -10295,6 +10716,49 @@ function getEmergingHubs(stateDb2, daysBack = 30) {
|
|
|
10295
10716
|
emerging.sort((a, b) => b.growth - a.growth);
|
|
10296
10717
|
return emerging;
|
|
10297
10718
|
}
|
|
10719
|
+
function compareGraphSnapshots(stateDb2, timestampBefore, timestampAfter) {
|
|
10720
|
+
const SCALAR_METRICS = ["avg_degree", "max_degree", "cluster_count", "largest_cluster_size"];
|
|
10721
|
+
function getSnapshotAt(ts) {
|
|
10722
|
+
const row = stateDb2.db.prepare(
|
|
10723
|
+
`SELECT DISTINCT timestamp FROM graph_snapshots WHERE timestamp <= ? ORDER BY timestamp DESC LIMIT 1`
|
|
10724
|
+
).get(ts);
|
|
10725
|
+
if (!row) return null;
|
|
10726
|
+
const rows = stateDb2.db.prepare(
|
|
10727
|
+
`SELECT metric, value, details FROM graph_snapshots WHERE timestamp = ?`
|
|
10728
|
+
).all(row.timestamp);
|
|
10729
|
+
return rows;
|
|
10730
|
+
}
|
|
10731
|
+
const beforeRows = getSnapshotAt(timestampBefore) ?? [];
|
|
10732
|
+
const afterRows = getSnapshotAt(timestampAfter) ?? [];
|
|
10733
|
+
const beforeMap = /* @__PURE__ */ new Map();
|
|
10734
|
+
const afterMap = /* @__PURE__ */ new Map();
|
|
10735
|
+
for (const r of beforeRows) beforeMap.set(r.metric, { value: r.value, details: r.details });
|
|
10736
|
+
for (const r of afterRows) afterMap.set(r.metric, { value: r.value, details: r.details });
|
|
10737
|
+
const metricChanges = SCALAR_METRICS.map((metric) => {
|
|
10738
|
+
const before = beforeMap.get(metric)?.value ?? 0;
|
|
10739
|
+
const after = afterMap.get(metric)?.value ?? 0;
|
|
10740
|
+
const delta = after - before;
|
|
10741
|
+
const deltaPercent = before !== 0 ? Math.round(delta / before * 1e4) / 100 : delta !== 0 ? 100 : 0;
|
|
10742
|
+
return { metric, before, after, delta, deltaPercent };
|
|
10743
|
+
});
|
|
10744
|
+
const beforeHubs = beforeMap.get("hub_scores_top10")?.details ? JSON.parse(beforeMap.get("hub_scores_top10").details) : [];
|
|
10745
|
+
const afterHubs = afterMap.get("hub_scores_top10")?.details ? JSON.parse(afterMap.get("hub_scores_top10").details) : [];
|
|
10746
|
+
const beforeHubMap = /* @__PURE__ */ new Map();
|
|
10747
|
+
for (const h of beforeHubs) beforeHubMap.set(h.entity, h.degree);
|
|
10748
|
+
const afterHubMap = /* @__PURE__ */ new Map();
|
|
10749
|
+
for (const h of afterHubs) afterHubMap.set(h.entity, h.degree);
|
|
10750
|
+
const allHubEntities = /* @__PURE__ */ new Set([...beforeHubMap.keys(), ...afterHubMap.keys()]);
|
|
10751
|
+
const hubScoreChanges = [];
|
|
10752
|
+
for (const entity of allHubEntities) {
|
|
10753
|
+
const before = beforeHubMap.get(entity) ?? 0;
|
|
10754
|
+
const after = afterHubMap.get(entity) ?? 0;
|
|
10755
|
+
if (before !== after) {
|
|
10756
|
+
hubScoreChanges.push({ entity, before, after, delta: after - before });
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
hubScoreChanges.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
|
|
10760
|
+
return { metricChanges, hubScoreChanges };
|
|
10761
|
+
}
|
|
10298
10762
|
function purgeOldSnapshots(stateDb2, retentionDays = 90) {
|
|
10299
10763
|
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
10300
10764
|
const result = stateDb2.db.prepare(
|
|
@@ -10469,7 +10933,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
10469
10933
|
const scored = allNotes.map((note) => {
|
|
10470
10934
|
let wordCount = 0;
|
|
10471
10935
|
try {
|
|
10472
|
-
const content = fs14.readFileSync(
|
|
10936
|
+
const content = fs14.readFileSync(path16.join(vaultPath2, note.path), "utf-8");
|
|
10473
10937
|
const body = content.replace(/^---[\s\S]*?---\n?/, "");
|
|
10474
10938
|
wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
|
|
10475
10939
|
} catch {
|
|
@@ -11049,12 +11513,12 @@ import { z as z10 } from "zod";
|
|
|
11049
11513
|
|
|
11050
11514
|
// src/tools/read/bidirectional.ts
|
|
11051
11515
|
import * as fs15 from "fs/promises";
|
|
11052
|
-
import * as
|
|
11516
|
+
import * as path17 from "path";
|
|
11053
11517
|
import matter3 from "gray-matter";
|
|
11054
11518
|
var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
|
|
11055
11519
|
var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
|
|
11056
11520
|
async function readFileContent2(notePath, vaultPath2) {
|
|
11057
|
-
const fullPath =
|
|
11521
|
+
const fullPath = path17.join(vaultPath2, notePath);
|
|
11058
11522
|
try {
|
|
11059
11523
|
return await fs15.readFile(fullPath, "utf-8");
|
|
11060
11524
|
} catch {
|
|
@@ -11233,10 +11697,10 @@ async function suggestWikilinksInFrontmatter(index, notePath, vaultPath2) {
|
|
|
11233
11697
|
|
|
11234
11698
|
// src/tools/read/computed.ts
|
|
11235
11699
|
import * as fs16 from "fs/promises";
|
|
11236
|
-
import * as
|
|
11700
|
+
import * as path18 from "path";
|
|
11237
11701
|
import matter4 from "gray-matter";
|
|
11238
11702
|
async function readFileContent3(notePath, vaultPath2) {
|
|
11239
|
-
const fullPath =
|
|
11703
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
11240
11704
|
try {
|
|
11241
11705
|
return await fs16.readFile(fullPath, "utf-8");
|
|
11242
11706
|
} catch {
|
|
@@ -11244,7 +11708,7 @@ async function readFileContent3(notePath, vaultPath2) {
|
|
|
11244
11708
|
}
|
|
11245
11709
|
}
|
|
11246
11710
|
async function getFileStats(notePath, vaultPath2) {
|
|
11247
|
-
const fullPath =
|
|
11711
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
11248
11712
|
try {
|
|
11249
11713
|
const stats = await fs16.stat(fullPath);
|
|
11250
11714
|
return {
|
|
@@ -11515,7 +11979,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath, getConfi
|
|
|
11515
11979
|
init_writer();
|
|
11516
11980
|
import { z as z11 } from "zod";
|
|
11517
11981
|
import fs20 from "fs/promises";
|
|
11518
|
-
import
|
|
11982
|
+
import path21 from "path";
|
|
11519
11983
|
|
|
11520
11984
|
// src/core/write/validator.ts
|
|
11521
11985
|
var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
|
|
@@ -11718,7 +12182,7 @@ function runValidationPipeline(content, format, options = {}) {
|
|
|
11718
12182
|
// src/core/write/mutation-helpers.ts
|
|
11719
12183
|
init_writer();
|
|
11720
12184
|
import fs19 from "fs/promises";
|
|
11721
|
-
import
|
|
12185
|
+
import path20 from "path";
|
|
11722
12186
|
init_constants();
|
|
11723
12187
|
init_writer();
|
|
11724
12188
|
function formatMcpResult(result) {
|
|
@@ -11767,7 +12231,7 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
|
|
|
11767
12231
|
return info;
|
|
11768
12232
|
}
|
|
11769
12233
|
async function ensureFileExists(vaultPath2, notePath) {
|
|
11770
|
-
const fullPath =
|
|
12234
|
+
const fullPath = path20.join(vaultPath2, notePath);
|
|
11771
12235
|
try {
|
|
11772
12236
|
await fs19.access(fullPath);
|
|
11773
12237
|
return null;
|
|
@@ -11826,7 +12290,7 @@ async function withVaultFile(options, operation) {
|
|
|
11826
12290
|
if ("error" in result) {
|
|
11827
12291
|
return formatMcpResult(result.error);
|
|
11828
12292
|
}
|
|
11829
|
-
const fullPath =
|
|
12293
|
+
const fullPath = path20.join(vaultPath2, notePath);
|
|
11830
12294
|
const statBefore = await fs19.stat(fullPath);
|
|
11831
12295
|
if (statBefore.mtimeMs !== result.mtimeMs) {
|
|
11832
12296
|
console.warn(`[withVaultFile] External modification detected on ${notePath}, re-reading and retrying`);
|
|
@@ -11889,10 +12353,10 @@ async function withVaultFrontmatter(options, operation) {
|
|
|
11889
12353
|
|
|
11890
12354
|
// src/tools/write/mutations.ts
|
|
11891
12355
|
async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
11892
|
-
const fullPath =
|
|
11893
|
-
await fs20.mkdir(
|
|
12356
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
12357
|
+
await fs20.mkdir(path21.dirname(fullPath), { recursive: true });
|
|
11894
12358
|
const templates = config.templates || {};
|
|
11895
|
-
const filename =
|
|
12359
|
+
const filename = path21.basename(notePath, ".md").toLowerCase();
|
|
11896
12360
|
let templatePath;
|
|
11897
12361
|
const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
|
|
11898
12362
|
const weeklyPattern = /^\d{4}-W\d{2}/;
|
|
@@ -11913,10 +12377,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
11913
12377
|
let templateContent;
|
|
11914
12378
|
if (templatePath) {
|
|
11915
12379
|
try {
|
|
11916
|
-
const absTemplatePath =
|
|
12380
|
+
const absTemplatePath = path21.join(vaultPath2, templatePath);
|
|
11917
12381
|
templateContent = await fs20.readFile(absTemplatePath, "utf-8");
|
|
11918
12382
|
} catch {
|
|
11919
|
-
const title =
|
|
12383
|
+
const title = path21.basename(notePath, ".md");
|
|
11920
12384
|
templateContent = `---
|
|
11921
12385
|
---
|
|
11922
12386
|
|
|
@@ -11925,7 +12389,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
11925
12389
|
templatePath = void 0;
|
|
11926
12390
|
}
|
|
11927
12391
|
} else {
|
|
11928
|
-
const title =
|
|
12392
|
+
const title = path21.basename(notePath, ".md");
|
|
11929
12393
|
templateContent = `---
|
|
11930
12394
|
---
|
|
11931
12395
|
|
|
@@ -11934,7 +12398,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
11934
12398
|
}
|
|
11935
12399
|
const now = /* @__PURE__ */ new Date();
|
|
11936
12400
|
const dateStr = now.toISOString().split("T")[0];
|
|
11937
|
-
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g,
|
|
12401
|
+
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path21.basename(notePath, ".md"));
|
|
11938
12402
|
const matter9 = (await import("gray-matter")).default;
|
|
11939
12403
|
const parsed = matter9(templateContent);
|
|
11940
12404
|
if (!parsed.data.date) {
|
|
@@ -11966,14 +12430,15 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
11966
12430
|
validate: z11.boolean().default(true).describe("Check input for common issues (double timestamps, non-markdown bullets, etc.)"),
|
|
11967
12431
|
normalize: z11.boolean().default(true).describe("Auto-fix common issues before formatting (replace \u2022 with -, trim excessive whitespace, etc.)"),
|
|
11968
12432
|
guardrails: z11.enum(["warn", "strict", "off"]).default("warn").describe('Output validation mode: "warn" returns issues but proceeds, "strict" blocks on errors, "off" disables'),
|
|
12433
|
+
linkedEntities: z11.array(z11.string()).optional().describe("Entity names already linked in the content. When skipWikilinks=true, these are tracked for feedback without re-processing the content."),
|
|
11969
12434
|
agent_id: z11.string().optional().describe('Agent identifier for multi-agent scoping (e.g., "claude-opus", "planning-agent")'),
|
|
11970
12435
|
session_id: z11.string().optional().describe('Session identifier for conversation scoping (e.g., "sess-abc123")')
|
|
11971
12436
|
},
|
|
11972
|
-
async ({ path: notePath, section, content, create_if_missing, position, format, commit, skipWikilinks, preserveListNesting, bumpHeadings, suggestOutgoingLinks, maxSuggestions, validate, normalize, guardrails, agent_id, session_id }) => {
|
|
12437
|
+
async ({ path: notePath, section, content, create_if_missing, position, format, commit, skipWikilinks, preserveListNesting, bumpHeadings, suggestOutgoingLinks, maxSuggestions, validate, normalize, guardrails, linkedEntities, agent_id, session_id }) => {
|
|
11973
12438
|
let noteCreated = false;
|
|
11974
12439
|
let templateUsed;
|
|
11975
12440
|
if (create_if_missing) {
|
|
11976
|
-
const fullPath =
|
|
12441
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
11977
12442
|
try {
|
|
11978
12443
|
await fs20.access(fullPath);
|
|
11979
12444
|
} catch {
|
|
@@ -12004,6 +12469,12 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
12004
12469
|
}
|
|
12005
12470
|
let workingContent = validationResult.content;
|
|
12006
12471
|
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(workingContent, skipWikilinks, notePath);
|
|
12472
|
+
if (linkedEntities?.length) {
|
|
12473
|
+
const stateDb2 = getWriteStateDb();
|
|
12474
|
+
if (stateDb2) {
|
|
12475
|
+
trackWikilinkApplications(stateDb2, notePath, linkedEntities);
|
|
12476
|
+
}
|
|
12477
|
+
}
|
|
12007
12478
|
const _debug = {
|
|
12008
12479
|
entityCount: getEntityIndexStats().totalEntities,
|
|
12009
12480
|
indexReady: getEntityIndexStats().ready,
|
|
@@ -12426,7 +12897,7 @@ Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { st
|
|
|
12426
12897
|
init_writer();
|
|
12427
12898
|
import { z as z14 } from "zod";
|
|
12428
12899
|
import fs21 from "fs/promises";
|
|
12429
|
-
import
|
|
12900
|
+
import path22 from "path";
|
|
12430
12901
|
function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
12431
12902
|
server2.tool(
|
|
12432
12903
|
"vault_create_note",
|
|
@@ -12439,7 +12910,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
12439
12910
|
overwrite: z14.boolean().default(false).describe("If true, overwrite existing file"),
|
|
12440
12911
|
commit: z14.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
|
|
12441
12912
|
skipWikilinks: z14.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
|
|
12442
|
-
suggestOutgoingLinks: z14.boolean().default(
|
|
12913
|
+
suggestOutgoingLinks: z14.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]], [[Philosophy]]").'),
|
|
12443
12914
|
maxSuggestions: z14.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)"),
|
|
12444
12915
|
agent_id: z14.string().optional().describe("Agent identifier for multi-agent scoping"),
|
|
12445
12916
|
session_id: z14.string().optional().describe("Session identifier for conversation scoping")
|
|
@@ -12449,23 +12920,23 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
12449
12920
|
if (!validatePath(vaultPath2, notePath)) {
|
|
12450
12921
|
return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
|
|
12451
12922
|
}
|
|
12452
|
-
const fullPath =
|
|
12923
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
12453
12924
|
const existsCheck = await ensureFileExists(vaultPath2, notePath);
|
|
12454
12925
|
if (existsCheck === null && !overwrite) {
|
|
12455
12926
|
return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
|
|
12456
12927
|
}
|
|
12457
|
-
const dir =
|
|
12928
|
+
const dir = path22.dirname(fullPath);
|
|
12458
12929
|
await fs21.mkdir(dir, { recursive: true });
|
|
12459
12930
|
let effectiveContent = content;
|
|
12460
12931
|
let effectiveFrontmatter = frontmatter;
|
|
12461
12932
|
if (template) {
|
|
12462
|
-
const templatePath =
|
|
12933
|
+
const templatePath = path22.join(vaultPath2, template);
|
|
12463
12934
|
try {
|
|
12464
12935
|
const raw = await fs21.readFile(templatePath, "utf-8");
|
|
12465
12936
|
const matter9 = (await import("gray-matter")).default;
|
|
12466
12937
|
const parsed = matter9(raw);
|
|
12467
12938
|
const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
12468
|
-
const title =
|
|
12939
|
+
const title = path22.basename(notePath, ".md");
|
|
12469
12940
|
let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
|
|
12470
12941
|
if (content) {
|
|
12471
12942
|
templateContent = templateContent.trimEnd() + "\n\n" + content;
|
|
@@ -12484,7 +12955,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
12484
12955
|
effectiveFrontmatter.created = now.toISOString();
|
|
12485
12956
|
}
|
|
12486
12957
|
const warnings = [];
|
|
12487
|
-
const noteName =
|
|
12958
|
+
const noteName = path22.basename(notePath, ".md");
|
|
12488
12959
|
const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
|
|
12489
12960
|
const preflight = await checkPreflightSimilarity(noteName);
|
|
12490
12961
|
if (preflight.existingEntity) {
|
|
@@ -12601,7 +13072,7 @@ ${sources}`;
|
|
|
12601
13072
|
}
|
|
12602
13073
|
return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
|
|
12603
13074
|
}
|
|
12604
|
-
const fullPath =
|
|
13075
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
12605
13076
|
await fs21.unlink(fullPath);
|
|
12606
13077
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
|
|
12607
13078
|
const message = backlinkWarning ? `Deleted note: ${notePath}
|
|
@@ -12621,7 +13092,7 @@ Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
|
|
|
12621
13092
|
init_writer();
|
|
12622
13093
|
import { z as z15 } from "zod";
|
|
12623
13094
|
import fs22 from "fs/promises";
|
|
12624
|
-
import
|
|
13095
|
+
import path23 from "path";
|
|
12625
13096
|
import matter6 from "gray-matter";
|
|
12626
13097
|
function escapeRegex(str) {
|
|
12627
13098
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -12640,7 +13111,7 @@ function extractWikilinks2(content) {
|
|
|
12640
13111
|
return wikilinks;
|
|
12641
13112
|
}
|
|
12642
13113
|
function getTitleFromPath(filePath) {
|
|
12643
|
-
return
|
|
13114
|
+
return path23.basename(filePath, ".md");
|
|
12644
13115
|
}
|
|
12645
13116
|
async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
12646
13117
|
const results = [];
|
|
@@ -12649,7 +13120,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
12649
13120
|
const files = [];
|
|
12650
13121
|
const entries = await fs22.readdir(dir, { withFileTypes: true });
|
|
12651
13122
|
for (const entry of entries) {
|
|
12652
|
-
const fullPath =
|
|
13123
|
+
const fullPath = path23.join(dir, entry.name);
|
|
12653
13124
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
12654
13125
|
files.push(...await scanDir(fullPath));
|
|
12655
13126
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -12660,7 +13131,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
12660
13131
|
}
|
|
12661
13132
|
const allFiles = await scanDir(vaultPath2);
|
|
12662
13133
|
for (const filePath of allFiles) {
|
|
12663
|
-
const relativePath =
|
|
13134
|
+
const relativePath = path23.relative(vaultPath2, filePath);
|
|
12664
13135
|
const content = await fs22.readFile(filePath, "utf-8");
|
|
12665
13136
|
const wikilinks = extractWikilinks2(content);
|
|
12666
13137
|
const matchingLinks = [];
|
|
@@ -12680,7 +13151,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
12680
13151
|
return results;
|
|
12681
13152
|
}
|
|
12682
13153
|
async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
|
|
12683
|
-
const fullPath =
|
|
13154
|
+
const fullPath = path23.join(vaultPath2, filePath);
|
|
12684
13155
|
const raw = await fs22.readFile(fullPath, "utf-8");
|
|
12685
13156
|
const parsed = matter6(raw);
|
|
12686
13157
|
let content = parsed.content;
|
|
@@ -12747,8 +13218,8 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
12747
13218
|
};
|
|
12748
13219
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
12749
13220
|
}
|
|
12750
|
-
const oldFullPath =
|
|
12751
|
-
const newFullPath =
|
|
13221
|
+
const oldFullPath = path23.join(vaultPath2, oldPath);
|
|
13222
|
+
const newFullPath = path23.join(vaultPath2, newPath);
|
|
12752
13223
|
try {
|
|
12753
13224
|
await fs22.access(oldFullPath);
|
|
12754
13225
|
} catch {
|
|
@@ -12798,7 +13269,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
12798
13269
|
}
|
|
12799
13270
|
}
|
|
12800
13271
|
}
|
|
12801
|
-
const destDir =
|
|
13272
|
+
const destDir = path23.dirname(newFullPath);
|
|
12802
13273
|
await fs22.mkdir(destDir, { recursive: true });
|
|
12803
13274
|
await fs22.rename(oldFullPath, newFullPath);
|
|
12804
13275
|
let gitCommit;
|
|
@@ -12884,10 +13355,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
12884
13355
|
if (sanitizedTitle !== newTitle) {
|
|
12885
13356
|
console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
|
|
12886
13357
|
}
|
|
12887
|
-
const fullPath =
|
|
12888
|
-
const dir =
|
|
12889
|
-
const newPath = dir === "." ? `${sanitizedTitle}.md` :
|
|
12890
|
-
const newFullPath =
|
|
13358
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
13359
|
+
const dir = path23.dirname(notePath);
|
|
13360
|
+
const newPath = dir === "." ? `${sanitizedTitle}.md` : path23.join(dir, `${sanitizedTitle}.md`);
|
|
13361
|
+
const newFullPath = path23.join(vaultPath2, newPath);
|
|
12891
13362
|
try {
|
|
12892
13363
|
await fs22.access(fullPath);
|
|
12893
13364
|
} catch {
|
|
@@ -13244,7 +13715,7 @@ init_schema();
|
|
|
13244
13715
|
// src/core/write/policy/parser.ts
|
|
13245
13716
|
init_schema();
|
|
13246
13717
|
import fs24 from "fs/promises";
|
|
13247
|
-
import
|
|
13718
|
+
import path24 from "path";
|
|
13248
13719
|
import matter7 from "gray-matter";
|
|
13249
13720
|
function parseYaml(content) {
|
|
13250
13721
|
const parsed = matter7(`---
|
|
@@ -13293,13 +13764,13 @@ async function loadPolicyFile(filePath) {
|
|
|
13293
13764
|
}
|
|
13294
13765
|
}
|
|
13295
13766
|
async function loadPolicy(vaultPath2, policyName) {
|
|
13296
|
-
const policiesDir =
|
|
13297
|
-
const policyPath =
|
|
13767
|
+
const policiesDir = path24.join(vaultPath2, ".claude", "policies");
|
|
13768
|
+
const policyPath = path24.join(policiesDir, `${policyName}.yaml`);
|
|
13298
13769
|
try {
|
|
13299
13770
|
await fs24.access(policyPath);
|
|
13300
13771
|
return loadPolicyFile(policyPath);
|
|
13301
13772
|
} catch {
|
|
13302
|
-
const ymlPath =
|
|
13773
|
+
const ymlPath = path24.join(policiesDir, `${policyName}.yml`);
|
|
13303
13774
|
try {
|
|
13304
13775
|
await fs24.access(ymlPath);
|
|
13305
13776
|
return loadPolicyFile(ymlPath);
|
|
@@ -13440,7 +13911,7 @@ init_conditions();
|
|
|
13440
13911
|
init_schema();
|
|
13441
13912
|
init_writer();
|
|
13442
13913
|
import fs26 from "fs/promises";
|
|
13443
|
-
import
|
|
13914
|
+
import path26 from "path";
|
|
13444
13915
|
init_constants();
|
|
13445
13916
|
async function executeStep(step, vaultPath2, context, conditionResults) {
|
|
13446
13917
|
const { execute, reason } = shouldStepExecute(step.when, conditionResults);
|
|
@@ -13456,12 +13927,17 @@ async function executeStep(step, vaultPath2, context, conditionResults) {
|
|
|
13456
13927
|
const resolvedParams = interpolateObject(step.params, context);
|
|
13457
13928
|
try {
|
|
13458
13929
|
const result = await executeToolCall(step.tool, resolvedParams, vaultPath2, context);
|
|
13930
|
+
const outputs = {};
|
|
13931
|
+
if (result.path) {
|
|
13932
|
+
outputs.path = result.path;
|
|
13933
|
+
}
|
|
13459
13934
|
return {
|
|
13460
13935
|
stepId: step.id,
|
|
13461
13936
|
success: result.success,
|
|
13462
13937
|
message: result.message,
|
|
13463
13938
|
path: result.path,
|
|
13464
|
-
preview: result.preview
|
|
13939
|
+
preview: result.preview,
|
|
13940
|
+
outputs
|
|
13465
13941
|
};
|
|
13466
13942
|
} catch (error) {
|
|
13467
13943
|
return {
|
|
@@ -13509,7 +13985,7 @@ async function executeAddToSection(params, vaultPath2, context) {
|
|
|
13509
13985
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
13510
13986
|
const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
|
|
13511
13987
|
const maxSuggestions = Number(params.maxSuggestions) || 3;
|
|
13512
|
-
const fullPath =
|
|
13988
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
13513
13989
|
try {
|
|
13514
13990
|
await fs26.access(fullPath);
|
|
13515
13991
|
} catch {
|
|
@@ -13549,7 +14025,7 @@ async function executeRemoveFromSection(params, vaultPath2) {
|
|
|
13549
14025
|
const pattern = String(params.pattern || "");
|
|
13550
14026
|
const mode = params.mode || "first";
|
|
13551
14027
|
const useRegex = Boolean(params.useRegex);
|
|
13552
|
-
const fullPath =
|
|
14028
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
13553
14029
|
try {
|
|
13554
14030
|
await fs26.access(fullPath);
|
|
13555
14031
|
} catch {
|
|
@@ -13580,7 +14056,7 @@ async function executeReplaceInSection(params, vaultPath2, context) {
|
|
|
13580
14056
|
const mode = params.mode || "first";
|
|
13581
14057
|
const useRegex = Boolean(params.useRegex);
|
|
13582
14058
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
13583
|
-
const fullPath =
|
|
14059
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
13584
14060
|
try {
|
|
13585
14061
|
await fs26.access(fullPath);
|
|
13586
14062
|
} catch {
|
|
@@ -13623,7 +14099,7 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
13623
14099
|
if (!validatePath(vaultPath2, notePath)) {
|
|
13624
14100
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
13625
14101
|
}
|
|
13626
|
-
const fullPath =
|
|
14102
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
13627
14103
|
try {
|
|
13628
14104
|
await fs26.access(fullPath);
|
|
13629
14105
|
if (!overwrite) {
|
|
@@ -13631,7 +14107,7 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
13631
14107
|
}
|
|
13632
14108
|
} catch {
|
|
13633
14109
|
}
|
|
13634
|
-
const dir =
|
|
14110
|
+
const dir = path26.dirname(fullPath);
|
|
13635
14111
|
await fs26.mkdir(dir, { recursive: true });
|
|
13636
14112
|
const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
|
|
13637
14113
|
await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
|
|
@@ -13651,7 +14127,7 @@ async function executeDeleteNote(params, vaultPath2) {
|
|
|
13651
14127
|
if (!validatePath(vaultPath2, notePath)) {
|
|
13652
14128
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
13653
14129
|
}
|
|
13654
|
-
const fullPath =
|
|
14130
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
13655
14131
|
try {
|
|
13656
14132
|
await fs26.access(fullPath);
|
|
13657
14133
|
} catch {
|
|
@@ -13668,7 +14144,7 @@ async function executeToggleTask(params, vaultPath2) {
|
|
|
13668
14144
|
const notePath = String(params.path || "");
|
|
13669
14145
|
const task = String(params.task || "");
|
|
13670
14146
|
const section = params.section ? String(params.section) : void 0;
|
|
13671
|
-
const fullPath =
|
|
14147
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
13672
14148
|
try {
|
|
13673
14149
|
await fs26.access(fullPath);
|
|
13674
14150
|
} catch {
|
|
@@ -13711,7 +14187,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
13711
14187
|
const completed = Boolean(params.completed);
|
|
13712
14188
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
13713
14189
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
13714
|
-
const fullPath =
|
|
14190
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
13715
14191
|
try {
|
|
13716
14192
|
await fs26.access(fullPath);
|
|
13717
14193
|
} catch {
|
|
@@ -13748,7 +14224,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
13748
14224
|
async function executeUpdateFrontmatter(params, vaultPath2) {
|
|
13749
14225
|
const notePath = String(params.path || "");
|
|
13750
14226
|
const updates = params.frontmatter || {};
|
|
13751
|
-
const fullPath =
|
|
14227
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
13752
14228
|
try {
|
|
13753
14229
|
await fs26.access(fullPath);
|
|
13754
14230
|
} catch {
|
|
@@ -13770,7 +14246,7 @@ async function executeAddFrontmatterField(params, vaultPath2) {
|
|
|
13770
14246
|
const notePath = String(params.path || "");
|
|
13771
14247
|
const key = String(params.key || "");
|
|
13772
14248
|
const value = params.value;
|
|
13773
|
-
const fullPath =
|
|
14249
|
+
const fullPath = path26.join(vaultPath2, notePath);
|
|
13774
14250
|
try {
|
|
13775
14251
|
await fs26.access(fullPath);
|
|
13776
14252
|
} catch {
|
|
@@ -13852,6 +14328,9 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
|
|
|
13852
14328
|
if (result.path && result.success && !result.skipped) {
|
|
13853
14329
|
filesModified.add(result.path);
|
|
13854
14330
|
}
|
|
14331
|
+
if (result.success && !result.skipped && result.outputs) {
|
|
14332
|
+
context.steps[step.id] = result.outputs;
|
|
14333
|
+
}
|
|
13855
14334
|
if (!result.success && !result.skipped) {
|
|
13856
14335
|
if (commit && filesModified.size > 0) {
|
|
13857
14336
|
await rollbackChanges(vaultPath2, originalContents, filesModified);
|
|
@@ -13930,7 +14409,7 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
|
|
|
13930
14409
|
async function rollbackChanges(vaultPath2, originalContents, filesModified) {
|
|
13931
14410
|
for (const filePath of filesModified) {
|
|
13932
14411
|
const original = originalContents.get(filePath);
|
|
13933
|
-
const fullPath =
|
|
14412
|
+
const fullPath = path26.join(vaultPath2, filePath);
|
|
13934
14413
|
if (original === null) {
|
|
13935
14414
|
try {
|
|
13936
14415
|
await fs26.unlink(fullPath);
|
|
@@ -13985,9 +14464,9 @@ async function previewPolicy(policy, vaultPath2, variables) {
|
|
|
13985
14464
|
|
|
13986
14465
|
// src/core/write/policy/storage.ts
|
|
13987
14466
|
import fs27 from "fs/promises";
|
|
13988
|
-
import
|
|
14467
|
+
import path27 from "path";
|
|
13989
14468
|
function getPoliciesDir(vaultPath2) {
|
|
13990
|
-
return
|
|
14469
|
+
return path27.join(vaultPath2, ".claude", "policies");
|
|
13991
14470
|
}
|
|
13992
14471
|
async function ensurePoliciesDir(vaultPath2) {
|
|
13993
14472
|
const dir = getPoliciesDir(vaultPath2);
|
|
@@ -14002,7 +14481,7 @@ async function listPolicies(vaultPath2) {
|
|
|
14002
14481
|
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
|
|
14003
14482
|
continue;
|
|
14004
14483
|
}
|
|
14005
|
-
const filePath =
|
|
14484
|
+
const filePath = path27.join(dir, file);
|
|
14006
14485
|
const stat3 = await fs27.stat(filePath);
|
|
14007
14486
|
const content = await fs27.readFile(filePath, "utf-8");
|
|
14008
14487
|
const metadata = extractPolicyMetadata(content);
|
|
@@ -14027,7 +14506,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
|
|
|
14027
14506
|
const dir = getPoliciesDir(vaultPath2);
|
|
14028
14507
|
await ensurePoliciesDir(vaultPath2);
|
|
14029
14508
|
const filename = `${policyName}.yaml`;
|
|
14030
|
-
const filePath =
|
|
14509
|
+
const filePath = path27.join(dir, filename);
|
|
14031
14510
|
if (!overwrite) {
|
|
14032
14511
|
try {
|
|
14033
14512
|
await fs27.access(filePath);
|
|
@@ -14571,7 +15050,7 @@ import { z as z20 } from "zod";
|
|
|
14571
15050
|
|
|
14572
15051
|
// src/core/write/tagRename.ts
|
|
14573
15052
|
import * as fs28 from "fs/promises";
|
|
14574
|
-
import * as
|
|
15053
|
+
import * as path28 from "path";
|
|
14575
15054
|
import matter8 from "gray-matter";
|
|
14576
15055
|
import { getProtectedZones } from "@velvetmonkey/vault-core";
|
|
14577
15056
|
function getNotesInFolder3(index, folder) {
|
|
@@ -14677,7 +15156,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
|
|
|
14677
15156
|
const previews = [];
|
|
14678
15157
|
let totalChanges = 0;
|
|
14679
15158
|
for (const note of affectedNotes) {
|
|
14680
|
-
const fullPath =
|
|
15159
|
+
const fullPath = path28.join(vaultPath2, note.path);
|
|
14681
15160
|
let fileContent;
|
|
14682
15161
|
try {
|
|
14683
15162
|
fileContent = await fs28.readFile(fullPath, "utf-8");
|
|
@@ -14812,21 +15291,26 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
14812
15291
|
"wikilink_feedback",
|
|
14813
15292
|
{
|
|
14814
15293
|
title: "Wikilink Feedback",
|
|
14815
|
-
description: 'Report and query wikilink accuracy feedback. Modes: "report" (record feedback), "list" (view recent feedback), "stats" (entity accuracy statistics), "dashboard" (full feedback loop data
|
|
15294
|
+
description: 'Report and query wikilink accuracy feedback. Modes: "report" (record feedback), "list" (view recent feedback), "stats" (entity accuracy statistics), "dashboard" (full feedback loop data), "entity_timeline" (score history for an entity), "layer_timeseries" (per-layer contribution over time), "snapshot_diff" (compare two graph snapshots).',
|
|
14816
15295
|
inputSchema: {
|
|
14817
|
-
mode: z21.enum(["report", "list", "stats", "dashboard"]).describe("Operation mode"),
|
|
14818
|
-
entity: z21.string().optional().describe("Entity name (required for report
|
|
15296
|
+
mode: z21.enum(["report", "list", "stats", "dashboard", "entity_timeline", "layer_timeseries", "snapshot_diff"]).describe("Operation mode"),
|
|
15297
|
+
entity: z21.string().optional().describe("Entity name (required for report and entity_timeline modes, optional filter for list/stats)"),
|
|
14819
15298
|
note_path: z21.string().optional().describe("Note path where the wikilink appeared (for report mode)"),
|
|
14820
15299
|
context: z21.string().optional().describe("Surrounding text context (for report mode)"),
|
|
14821
15300
|
correct: z21.boolean().optional().describe("Whether the wikilink was correct (for report mode)"),
|
|
14822
|
-
limit: z21.number().optional().describe("Max entries to return for list
|
|
15301
|
+
limit: z21.number().optional().describe("Max entries to return (default: 20 for list, 100 for entity_timeline)"),
|
|
15302
|
+
days_back: z21.number().optional().describe("Days to look back (default: 30)"),
|
|
15303
|
+
granularity: z21.enum(["day", "week"]).optional().describe("Time bucket granularity for layer_timeseries (default: day)"),
|
|
15304
|
+
timestamp_before: z21.number().optional().describe("Earlier timestamp for snapshot_diff"),
|
|
15305
|
+
timestamp_after: z21.number().optional().describe("Later timestamp for snapshot_diff")
|
|
14823
15306
|
}
|
|
14824
15307
|
},
|
|
14825
|
-
async ({ mode, entity, note_path, context, correct, limit }) => {
|
|
15308
|
+
async ({ mode, entity, note_path, context, correct, limit, days_back, granularity, timestamp_before, timestamp_after }) => {
|
|
14826
15309
|
const stateDb2 = getStateDb();
|
|
14827
15310
|
if (!stateDb2) {
|
|
14828
15311
|
return {
|
|
14829
|
-
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
15312
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available \u2014 database not initialized yet" }) }],
|
|
15313
|
+
isError: true
|
|
14830
15314
|
};
|
|
14831
15315
|
}
|
|
14832
15316
|
let result;
|
|
@@ -14837,7 +15321,15 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
14837
15321
|
content: [{ type: "text", text: JSON.stringify({ error: "entity and correct are required for report mode" }) }]
|
|
14838
15322
|
};
|
|
14839
15323
|
}
|
|
14840
|
-
|
|
15324
|
+
try {
|
|
15325
|
+
recordFeedback(stateDb2, entity, context || "", note_path || "", correct);
|
|
15326
|
+
} catch (e) {
|
|
15327
|
+
return {
|
|
15328
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
15329
|
+
error: `Failed to record feedback: ${e instanceof Error ? e.message : String(e)}`
|
|
15330
|
+
}) }]
|
|
15331
|
+
};
|
|
15332
|
+
}
|
|
14841
15333
|
const suppressionUpdated = updateSuppressionList(stateDb2) > 0;
|
|
14842
15334
|
result = {
|
|
14843
15335
|
mode: "report",
|
|
@@ -14870,7 +15362,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
14870
15362
|
break;
|
|
14871
15363
|
}
|
|
14872
15364
|
case "dashboard": {
|
|
14873
|
-
const dashboard =
|
|
15365
|
+
const dashboard = getExtendedDashboardData(stateDb2);
|
|
14874
15366
|
result = {
|
|
14875
15367
|
mode: "dashboard",
|
|
14876
15368
|
dashboard,
|
|
@@ -14879,6 +15371,44 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
14879
15371
|
};
|
|
14880
15372
|
break;
|
|
14881
15373
|
}
|
|
15374
|
+
case "entity_timeline": {
|
|
15375
|
+
if (!entity) {
|
|
15376
|
+
return {
|
|
15377
|
+
content: [{ type: "text", text: JSON.stringify({ error: "entity is required for entity_timeline mode" }) }]
|
|
15378
|
+
};
|
|
15379
|
+
}
|
|
15380
|
+
const timeline = getEntityScoreTimeline(stateDb2, entity, days_back ?? 30, limit ?? 100);
|
|
15381
|
+
result = {
|
|
15382
|
+
mode: "entity_timeline",
|
|
15383
|
+
entity,
|
|
15384
|
+
timeline,
|
|
15385
|
+
count: timeline.length
|
|
15386
|
+
};
|
|
15387
|
+
break;
|
|
15388
|
+
}
|
|
15389
|
+
case "layer_timeseries": {
|
|
15390
|
+
const timeseries = getLayerContributionTimeseries(stateDb2, granularity ?? "day", days_back ?? 30);
|
|
15391
|
+
result = {
|
|
15392
|
+
mode: "layer_timeseries",
|
|
15393
|
+
granularity: granularity ?? "day",
|
|
15394
|
+
timeseries,
|
|
15395
|
+
buckets: timeseries.length
|
|
15396
|
+
};
|
|
15397
|
+
break;
|
|
15398
|
+
}
|
|
15399
|
+
case "snapshot_diff": {
|
|
15400
|
+
if (!timestamp_before || !timestamp_after) {
|
|
15401
|
+
return {
|
|
15402
|
+
content: [{ type: "text", text: JSON.stringify({ error: "timestamp_before and timestamp_after are required for snapshot_diff mode" }) }]
|
|
15403
|
+
};
|
|
15404
|
+
}
|
|
15405
|
+
const diff = compareGraphSnapshots(stateDb2, timestamp_before, timestamp_after);
|
|
15406
|
+
result = {
|
|
15407
|
+
mode: "snapshot_diff",
|
|
15408
|
+
diff
|
|
15409
|
+
};
|
|
15410
|
+
break;
|
|
15411
|
+
}
|
|
14882
15412
|
}
|
|
14883
15413
|
return {
|
|
14884
15414
|
content: [
|
|
@@ -15264,8 +15794,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
|
15264
15794
|
}
|
|
15265
15795
|
}
|
|
15266
15796
|
}
|
|
15267
|
-
return Array.from(noteMap.entries()).map(([
|
|
15268
|
-
path:
|
|
15797
|
+
return Array.from(noteMap.entries()).map(([path31, stats]) => ({
|
|
15798
|
+
path: path31,
|
|
15269
15799
|
access_count: stats.access_count,
|
|
15270
15800
|
last_accessed: stats.last_accessed,
|
|
15271
15801
|
tools_used: Array.from(stats.tools)
|
|
@@ -15418,7 +15948,7 @@ import { z as z25 } from "zod";
|
|
|
15418
15948
|
|
|
15419
15949
|
// src/core/read/similarity.ts
|
|
15420
15950
|
import * as fs29 from "fs";
|
|
15421
|
-
import * as
|
|
15951
|
+
import * as path29 from "path";
|
|
15422
15952
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
15423
15953
|
"the",
|
|
15424
15954
|
"be",
|
|
@@ -15555,7 +16085,7 @@ function extractKeyTerms(content, maxTerms = 15) {
|
|
|
15555
16085
|
}
|
|
15556
16086
|
function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
|
|
15557
16087
|
const limit = options.limit ?? 10;
|
|
15558
|
-
const absPath =
|
|
16088
|
+
const absPath = path29.join(vaultPath2, sourcePath);
|
|
15559
16089
|
let content;
|
|
15560
16090
|
try {
|
|
15561
16091
|
content = fs29.readFileSync(absPath, "utf-8");
|
|
@@ -15683,7 +16213,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
15683
16213
|
exclude_linked: z25.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
|
|
15684
16214
|
}
|
|
15685
16215
|
},
|
|
15686
|
-
async ({ path:
|
|
16216
|
+
async ({ path: path31, limit, exclude_linked }) => {
|
|
15687
16217
|
const index = getIndex();
|
|
15688
16218
|
const vaultPath2 = getVaultPath();
|
|
15689
16219
|
const stateDb2 = getStateDb();
|
|
@@ -15692,10 +16222,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
15692
16222
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
15693
16223
|
};
|
|
15694
16224
|
}
|
|
15695
|
-
if (!index.notes.has(
|
|
16225
|
+
if (!index.notes.has(path31)) {
|
|
15696
16226
|
return {
|
|
15697
16227
|
content: [{ type: "text", text: JSON.stringify({
|
|
15698
|
-
error: `Note not found: ${
|
|
16228
|
+
error: `Note not found: ${path31}`,
|
|
15699
16229
|
hint: "Use the full relative path including .md extension"
|
|
15700
16230
|
}, null, 2) }]
|
|
15701
16231
|
};
|
|
@@ -15706,12 +16236,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
15706
16236
|
};
|
|
15707
16237
|
const useHybrid = hasEmbeddingsIndex();
|
|
15708
16238
|
const method = useHybrid ? "hybrid" : "bm25";
|
|
15709
|
-
const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index,
|
|
16239
|
+
const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path31, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path31, opts);
|
|
15710
16240
|
return {
|
|
15711
16241
|
content: [{
|
|
15712
16242
|
type: "text",
|
|
15713
16243
|
text: JSON.stringify({
|
|
15714
|
-
source:
|
|
16244
|
+
source: path31,
|
|
15715
16245
|
method,
|
|
15716
16246
|
exclude_linked: exclude_linked ?? true,
|
|
15717
16247
|
count: results.length,
|
|
@@ -15949,6 +16479,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
15949
16479
|
|
|
15950
16480
|
// src/index.ts
|
|
15951
16481
|
import * as fs30 from "node:fs/promises";
|
|
16482
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
15952
16483
|
|
|
15953
16484
|
// src/resources/vault.ts
|
|
15954
16485
|
function registerVaultResources(server2, getIndex) {
|
|
@@ -16054,7 +16585,16 @@ function registerVaultResources(server2, getIndex) {
|
|
|
16054
16585
|
}
|
|
16055
16586
|
|
|
16056
16587
|
// src/index.ts
|
|
16588
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
16589
|
+
var __dirname = dirname4(__filename);
|
|
16590
|
+
var pkg = JSON.parse(readFileSync4(join16(__dirname, "../package.json"), "utf-8"));
|
|
16057
16591
|
var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
16592
|
+
var resolvedVaultPath;
|
|
16593
|
+
try {
|
|
16594
|
+
resolvedVaultPath = realpathSync(vaultPath).replace(/\\/g, "/");
|
|
16595
|
+
} catch {
|
|
16596
|
+
resolvedVaultPath = vaultPath.replace(/\\/g, "/");
|
|
16597
|
+
}
|
|
16058
16598
|
var vaultIndex;
|
|
16059
16599
|
var flywheelConfig = {};
|
|
16060
16600
|
var stateDb = null;
|
|
@@ -16208,7 +16748,7 @@ var TOOL_CATEGORY = {
|
|
|
16208
16748
|
};
|
|
16209
16749
|
var server = new McpServer({
|
|
16210
16750
|
name: "flywheel-memory",
|
|
16211
|
-
version:
|
|
16751
|
+
version: pkg.version
|
|
16212
16752
|
});
|
|
16213
16753
|
var _registeredCount = 0;
|
|
16214
16754
|
var _skippedCount = 0;
|
|
@@ -16337,7 +16877,7 @@ registerMergeTools2(server, () => stateDb);
|
|
|
16337
16877
|
registerVaultResources(server, () => vaultIndex ?? null);
|
|
16338
16878
|
serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
16339
16879
|
async function main() {
|
|
16340
|
-
serverLog("server",
|
|
16880
|
+
serverLog("server", `Starting Flywheel Memory v${pkg.version}...`);
|
|
16341
16881
|
serverLog("server", `Vault: ${vaultPath}`);
|
|
16342
16882
|
const startTime = Date.now();
|
|
16343
16883
|
try {
|
|
@@ -16349,6 +16889,7 @@ async function main() {
|
|
|
16349
16889
|
serverLog("statedb", "Injected FTS5, embeddings, task cache handles");
|
|
16350
16890
|
loadEntityEmbeddingsToMemory();
|
|
16351
16891
|
setWriteStateDb(stateDb);
|
|
16892
|
+
setRecencyStateDb(stateDb);
|
|
16352
16893
|
} catch (err) {
|
|
16353
16894
|
const msg = err instanceof Error ? err.message : String(err);
|
|
16354
16895
|
serverLog("statedb", `StateDb initialization failed: ${msg}`, "error");
|
|
@@ -16549,21 +17090,105 @@ async function runPostIndexWork(index) {
|
|
|
16549
17090
|
}
|
|
16550
17091
|
if (process.env.FLYWHEEL_WATCH !== "false") {
|
|
16551
17092
|
const config = parseWatcherConfig();
|
|
17093
|
+
const lastContentHashes = /* @__PURE__ */ new Map();
|
|
16552
17094
|
serverLog("watcher", `File watcher enabled (debounce: ${config.debounceMs}ms)`);
|
|
16553
17095
|
const watcher = createVaultWatcher({
|
|
16554
17096
|
vaultPath,
|
|
16555
17097
|
config,
|
|
16556
17098
|
onBatch: async (batch) => {
|
|
16557
|
-
|
|
17099
|
+
const vaultPrefixes = /* @__PURE__ */ new Set([
|
|
17100
|
+
vaultPath.replace(/\\/g, "/"),
|
|
17101
|
+
resolvedVaultPath
|
|
17102
|
+
]);
|
|
17103
|
+
for (const event of batch.events) {
|
|
17104
|
+
const normalized = event.path.replace(/\\/g, "/");
|
|
17105
|
+
let matched = false;
|
|
17106
|
+
for (const prefix of vaultPrefixes) {
|
|
17107
|
+
if (normalized.startsWith(prefix + "/")) {
|
|
17108
|
+
event.path = normalized.slice(prefix.length + 1);
|
|
17109
|
+
matched = true;
|
|
17110
|
+
break;
|
|
17111
|
+
}
|
|
17112
|
+
}
|
|
17113
|
+
if (!matched) {
|
|
17114
|
+
try {
|
|
17115
|
+
const resolved = realpathSync(event.path).replace(/\\/g, "/");
|
|
17116
|
+
for (const prefix of vaultPrefixes) {
|
|
17117
|
+
if (resolved.startsWith(prefix + "/")) {
|
|
17118
|
+
event.path = resolved.slice(prefix.length + 1);
|
|
17119
|
+
matched = true;
|
|
17120
|
+
break;
|
|
17121
|
+
}
|
|
17122
|
+
}
|
|
17123
|
+
} catch {
|
|
17124
|
+
try {
|
|
17125
|
+
const dir = path30.dirname(event.path);
|
|
17126
|
+
const base = path30.basename(event.path);
|
|
17127
|
+
const resolvedDir = realpathSync(dir).replace(/\\/g, "/");
|
|
17128
|
+
for (const prefix of vaultPrefixes) {
|
|
17129
|
+
if (resolvedDir.startsWith(prefix + "/") || resolvedDir === prefix) {
|
|
17130
|
+
const relDir = resolvedDir === prefix ? "" : resolvedDir.slice(prefix.length + 1);
|
|
17131
|
+
event.path = relDir ? `${relDir}/${base}` : base;
|
|
17132
|
+
matched = true;
|
|
17133
|
+
break;
|
|
17134
|
+
}
|
|
17135
|
+
}
|
|
17136
|
+
} catch {
|
|
17137
|
+
}
|
|
17138
|
+
}
|
|
17139
|
+
}
|
|
17140
|
+
}
|
|
17141
|
+
const filteredEvents = [];
|
|
17142
|
+
for (const event of batch.events) {
|
|
17143
|
+
if (event.type === "delete") {
|
|
17144
|
+
filteredEvents.push(event);
|
|
17145
|
+
lastContentHashes.delete(event.path);
|
|
17146
|
+
continue;
|
|
17147
|
+
}
|
|
17148
|
+
try {
|
|
17149
|
+
const content = await fs30.readFile(path30.join(vaultPath, event.path), "utf-8");
|
|
17150
|
+
const hash = createHash2("md5").update(content).digest("hex");
|
|
17151
|
+
if (lastContentHashes.get(event.path) === hash) {
|
|
17152
|
+
serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
|
|
17153
|
+
continue;
|
|
17154
|
+
}
|
|
17155
|
+
lastContentHashes.set(event.path, hash);
|
|
17156
|
+
filteredEvents.push(event);
|
|
17157
|
+
} catch {
|
|
17158
|
+
filteredEvents.push(event);
|
|
17159
|
+
}
|
|
17160
|
+
}
|
|
17161
|
+
if (filteredEvents.length === 0) {
|
|
17162
|
+
serverLog("watcher", "All files unchanged (hash gate), skipping batch");
|
|
17163
|
+
return;
|
|
17164
|
+
}
|
|
17165
|
+
serverLog("watcher", `Processing ${filteredEvents.length} file changes`);
|
|
16558
17166
|
const batchStart = Date.now();
|
|
16559
|
-
const changedPaths =
|
|
17167
|
+
const changedPaths = filteredEvents.map((e) => e.path);
|
|
16560
17168
|
const tracker = createStepTracker();
|
|
16561
17169
|
try {
|
|
16562
|
-
tracker.start("index_rebuild", { files_changed:
|
|
16563
|
-
|
|
17170
|
+
tracker.start("index_rebuild", { files_changed: filteredEvents.length, changed_paths: changedPaths });
|
|
17171
|
+
if (!vaultIndex) {
|
|
17172
|
+
vaultIndex = await buildVaultIndex(vaultPath);
|
|
17173
|
+
serverLog("watcher", `Index rebuilt (full): ${vaultIndex.notes.size} notes, ${vaultIndex.entities.size} entities`);
|
|
17174
|
+
} else {
|
|
17175
|
+
const absoluteBatch = {
|
|
17176
|
+
...batch,
|
|
17177
|
+
events: filteredEvents.map((e) => ({
|
|
17178
|
+
...e,
|
|
17179
|
+
path: path30.join(vaultPath, e.path)
|
|
17180
|
+
}))
|
|
17181
|
+
};
|
|
17182
|
+
const batchResult = await processBatch(vaultIndex, vaultPath, absoluteBatch);
|
|
17183
|
+
serverLog("watcher", `Incremental: ${batchResult.successful}/${batchResult.total} files in ${batchResult.durationMs}ms`);
|
|
17184
|
+
}
|
|
16564
17185
|
setIndexState("ready");
|
|
16565
17186
|
tracker.end({ note_count: vaultIndex.notes.size, entity_count: vaultIndex.entities.size, tag_count: vaultIndex.tags.size });
|
|
16566
|
-
|
|
17187
|
+
const hubBefore = /* @__PURE__ */ new Map();
|
|
17188
|
+
if (stateDb) {
|
|
17189
|
+
const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
|
|
17190
|
+
for (const r of rows) hubBefore.set(r.name, r.hub_score);
|
|
17191
|
+
}
|
|
16567
17192
|
const entitiesBefore = stateDb ? getAllEntitiesFromDb3(stateDb) : [];
|
|
16568
17193
|
tracker.start("entity_scan", { note_count: vaultIndex.notes.size });
|
|
16569
17194
|
await updateEntitiesInStateDb();
|
|
@@ -16571,11 +17196,6 @@ async function runPostIndexWork(index) {
|
|
|
16571
17196
|
const entityDiff = computeEntityDiff(entitiesBefore, entitiesAfter);
|
|
16572
17197
|
tracker.end({ entity_count: entitiesAfter.length, ...entityDiff });
|
|
16573
17198
|
serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
|
|
16574
|
-
const hubBefore = /* @__PURE__ */ new Map();
|
|
16575
|
-
if (stateDb) {
|
|
16576
|
-
const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
|
|
16577
|
-
for (const r of rows) hubBefore.set(r.name, r.hub_score);
|
|
16578
|
-
}
|
|
16579
17199
|
tracker.start("hub_scores", { entity_count: entitiesAfter.length });
|
|
16580
17200
|
const hubUpdated = await exportHubScores(vaultIndex, stateDb);
|
|
16581
17201
|
const hubDiffs = [];
|
|
@@ -16588,17 +17208,35 @@ async function runPostIndexWork(index) {
|
|
|
16588
17208
|
}
|
|
16589
17209
|
tracker.end({ updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) });
|
|
16590
17210
|
serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
|
|
17211
|
+
tracker.start("recency", { entity_count: entitiesAfter.length });
|
|
17212
|
+
try {
|
|
17213
|
+
const cachedRecency = loadRecencyFromStateDb();
|
|
17214
|
+
const cacheAgeMs = cachedRecency ? Date.now() - (cachedRecency.lastUpdated ?? 0) : Infinity;
|
|
17215
|
+
if (cacheAgeMs >= 60 * 60 * 1e3) {
|
|
17216
|
+
const entities = entitiesAfter.map((e) => ({ name: e.name, path: e.path, aliases: e.aliases }));
|
|
17217
|
+
const recencyIndex2 = await buildRecencyIndex(vaultPath, entities);
|
|
17218
|
+
saveRecencyToStateDb(recencyIndex2);
|
|
17219
|
+
tracker.end({ rebuilt: true, entities: recencyIndex2.lastMentioned.size });
|
|
17220
|
+
serverLog("watcher", `Recency: rebuilt ${recencyIndex2.lastMentioned.size} entities`);
|
|
17221
|
+
} else {
|
|
17222
|
+
tracker.end({ rebuilt: false, cached_age_ms: cacheAgeMs });
|
|
17223
|
+
serverLog("watcher", `Recency: cache valid (${Math.round(cacheAgeMs / 1e3)}s old)`);
|
|
17224
|
+
}
|
|
17225
|
+
} catch (e) {
|
|
17226
|
+
tracker.end({ error: String(e) });
|
|
17227
|
+
serverLog("watcher", `Recency: failed: ${e}`);
|
|
17228
|
+
}
|
|
16591
17229
|
if (hasEmbeddingsIndex()) {
|
|
16592
|
-
tracker.start("note_embeddings", { files:
|
|
17230
|
+
tracker.start("note_embeddings", { files: filteredEvents.length });
|
|
16593
17231
|
let embUpdated = 0;
|
|
16594
17232
|
let embRemoved = 0;
|
|
16595
|
-
for (const event of
|
|
17233
|
+
for (const event of filteredEvents) {
|
|
16596
17234
|
try {
|
|
16597
17235
|
if (event.type === "delete") {
|
|
16598
17236
|
removeEmbedding(event.path);
|
|
16599
17237
|
embRemoved++;
|
|
16600
17238
|
} else if (event.path.endsWith(".md")) {
|
|
16601
|
-
const absPath =
|
|
17239
|
+
const absPath = path30.join(vaultPath, event.path);
|
|
16602
17240
|
await updateEmbedding(event.path, absPath);
|
|
16603
17241
|
embUpdated++;
|
|
16604
17242
|
}
|
|
@@ -16611,12 +17249,12 @@ async function runPostIndexWork(index) {
|
|
|
16611
17249
|
tracker.skip("note_embeddings", "not built");
|
|
16612
17250
|
}
|
|
16613
17251
|
if (hasEntityEmbeddingsIndex() && stateDb) {
|
|
16614
|
-
tracker.start("entity_embeddings", { files:
|
|
17252
|
+
tracker.start("entity_embeddings", { files: filteredEvents.length });
|
|
16615
17253
|
let entEmbUpdated = 0;
|
|
16616
17254
|
const entEmbNames = [];
|
|
16617
17255
|
try {
|
|
16618
17256
|
const allEntities = getAllEntitiesFromDb3(stateDb);
|
|
16619
|
-
for (const event of
|
|
17257
|
+
for (const event of filteredEvents) {
|
|
16620
17258
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
16621
17259
|
const matching = allEntities.filter((e) => e.path === event.path);
|
|
16622
17260
|
for (const entity of matching) {
|
|
@@ -16650,10 +17288,10 @@ async function runPostIndexWork(index) {
|
|
|
16650
17288
|
} else {
|
|
16651
17289
|
tracker.skip("index_cache", "no stateDb");
|
|
16652
17290
|
}
|
|
16653
|
-
tracker.start("task_cache", { files:
|
|
17291
|
+
tracker.start("task_cache", { files: filteredEvents.length });
|
|
16654
17292
|
let taskUpdated = 0;
|
|
16655
17293
|
let taskRemoved = 0;
|
|
16656
|
-
for (const event of
|
|
17294
|
+
for (const event of filteredEvents) {
|
|
16657
17295
|
try {
|
|
16658
17296
|
if (event.type === "delete") {
|
|
16659
17297
|
removeTaskCacheForFile(event.path);
|
|
@@ -16667,10 +17305,42 @@ async function runPostIndexWork(index) {
|
|
|
16667
17305
|
}
|
|
16668
17306
|
tracker.end({ updated: taskUpdated, removed: taskRemoved });
|
|
16669
17307
|
serverLog("watcher", `Task cache: ${taskUpdated} updated, ${taskRemoved} removed`);
|
|
16670
|
-
tracker.start("
|
|
17308
|
+
tracker.start("forward_links", { files: filteredEvents.length });
|
|
17309
|
+
const forwardLinkResults = [];
|
|
17310
|
+
let totalResolved = 0;
|
|
17311
|
+
let totalDead = 0;
|
|
17312
|
+
for (const event of filteredEvents) {
|
|
17313
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
17314
|
+
try {
|
|
17315
|
+
const links = getForwardLinksForNote(vaultIndex, event.path);
|
|
17316
|
+
const resolved = [];
|
|
17317
|
+
const dead = [];
|
|
17318
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17319
|
+
for (const link of links) {
|
|
17320
|
+
const name = link.target;
|
|
17321
|
+
if (seen.has(name.toLowerCase())) continue;
|
|
17322
|
+
seen.add(name.toLowerCase());
|
|
17323
|
+
if (link.exists) resolved.push(name);
|
|
17324
|
+
else dead.push(name);
|
|
17325
|
+
}
|
|
17326
|
+
if (resolved.length > 0 || dead.length > 0) {
|
|
17327
|
+
forwardLinkResults.push({ file: event.path, resolved, dead });
|
|
17328
|
+
}
|
|
17329
|
+
totalResolved += resolved.length;
|
|
17330
|
+
totalDead += dead.length;
|
|
17331
|
+
} catch {
|
|
17332
|
+
}
|
|
17333
|
+
}
|
|
17334
|
+
tracker.end({
|
|
17335
|
+
total_resolved: totalResolved,
|
|
17336
|
+
total_dead: totalDead,
|
|
17337
|
+
links: forwardLinkResults
|
|
17338
|
+
});
|
|
17339
|
+
serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead`);
|
|
17340
|
+
tracker.start("wikilink_check", { files: filteredEvents.length });
|
|
16671
17341
|
const trackedLinks = [];
|
|
16672
17342
|
if (stateDb) {
|
|
16673
|
-
for (const event of
|
|
17343
|
+
for (const event of filteredEvents) {
|
|
16674
17344
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
16675
17345
|
try {
|
|
16676
17346
|
const apps = getTrackedApplications(stateDb, event.path);
|
|
@@ -16681,13 +17351,13 @@ async function runPostIndexWork(index) {
|
|
|
16681
17351
|
}
|
|
16682
17352
|
tracker.end({ tracked: trackedLinks });
|
|
16683
17353
|
serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files`);
|
|
16684
|
-
tracker.start("implicit_feedback", { files:
|
|
17354
|
+
tracker.start("implicit_feedback", { files: filteredEvents.length });
|
|
16685
17355
|
const feedbackResults = [];
|
|
16686
17356
|
if (stateDb) {
|
|
16687
|
-
for (const event of
|
|
17357
|
+
for (const event of filteredEvents) {
|
|
16688
17358
|
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
16689
17359
|
try {
|
|
16690
|
-
const content = await fs30.readFile(
|
|
17360
|
+
const content = await fs30.readFile(path30.join(vaultPath, event.path), "utf-8");
|
|
16691
17361
|
const removed = processImplicitFeedback(stateDb, event.path, content);
|
|
16692
17362
|
for (const entity of removed) feedbackResults.push({ entity, file: event.path });
|
|
16693
17363
|
} catch {
|
|
@@ -16704,12 +17374,12 @@ async function runPostIndexWork(index) {
|
|
|
16704
17374
|
trigger: "watcher",
|
|
16705
17375
|
duration_ms: duration,
|
|
16706
17376
|
note_count: vaultIndex.notes.size,
|
|
16707
|
-
files_changed:
|
|
17377
|
+
files_changed: filteredEvents.length,
|
|
16708
17378
|
changed_paths: changedPaths,
|
|
16709
17379
|
steps: tracker.steps
|
|
16710
17380
|
});
|
|
16711
17381
|
}
|
|
16712
|
-
serverLog("watcher", `Batch complete: ${
|
|
17382
|
+
serverLog("watcher", `Batch complete: ${filteredEvents.length} files, ${duration}ms, ${tracker.steps.length} steps`);
|
|
16713
17383
|
} catch (err) {
|
|
16714
17384
|
setIndexState("error");
|
|
16715
17385
|
setIndexError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -16719,7 +17389,7 @@ async function runPostIndexWork(index) {
|
|
|
16719
17389
|
trigger: "watcher",
|
|
16720
17390
|
duration_ms: duration,
|
|
16721
17391
|
success: false,
|
|
16722
|
-
files_changed:
|
|
17392
|
+
files_changed: filteredEvents.length,
|
|
16723
17393
|
changed_paths: changedPaths,
|
|
16724
17394
|
error: err instanceof Error ? err.message : String(err),
|
|
16725
17395
|
steps: tracker.steps
|