@velvetmonkey/flywheel-memory 2.0.17 → 2.0.19
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 +2490 -967
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -23,8 +23,8 @@ var init_constants = __esm({
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
// src/core/write/writer.ts
|
|
26
|
-
import
|
|
27
|
-
import
|
|
26
|
+
import fs17 from "fs/promises";
|
|
27
|
+
import path16 from "path";
|
|
28
28
|
import matter5 from "gray-matter";
|
|
29
29
|
function isSensitivePath(filePath) {
|
|
30
30
|
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
@@ -345,8 +345,8 @@ function validatePath(vaultPath2, notePath) {
|
|
|
345
345
|
if (notePath.startsWith("\\")) {
|
|
346
346
|
return false;
|
|
347
347
|
}
|
|
348
|
-
const resolvedVault =
|
|
349
|
-
const resolvedNote =
|
|
348
|
+
const resolvedVault = path16.resolve(vaultPath2);
|
|
349
|
+
const resolvedNote = path16.resolve(vaultPath2, notePath);
|
|
350
350
|
return resolvedNote.startsWith(resolvedVault);
|
|
351
351
|
}
|
|
352
352
|
async function validatePathSecure(vaultPath2, notePath) {
|
|
@@ -374,8 +374,8 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
374
374
|
reason: "Path traversal not allowed"
|
|
375
375
|
};
|
|
376
376
|
}
|
|
377
|
-
const resolvedVault =
|
|
378
|
-
const resolvedNote =
|
|
377
|
+
const resolvedVault = path16.resolve(vaultPath2);
|
|
378
|
+
const resolvedNote = path16.resolve(vaultPath2, notePath);
|
|
379
379
|
if (!resolvedNote.startsWith(resolvedVault)) {
|
|
380
380
|
return {
|
|
381
381
|
valid: false,
|
|
@@ -389,18 +389,18 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
389
389
|
};
|
|
390
390
|
}
|
|
391
391
|
try {
|
|
392
|
-
const fullPath =
|
|
392
|
+
const fullPath = path16.join(vaultPath2, notePath);
|
|
393
393
|
try {
|
|
394
|
-
await
|
|
395
|
-
const realPath = await
|
|
396
|
-
const realVaultPath = await
|
|
394
|
+
await fs17.access(fullPath);
|
|
395
|
+
const realPath = await fs17.realpath(fullPath);
|
|
396
|
+
const realVaultPath = await fs17.realpath(vaultPath2);
|
|
397
397
|
if (!realPath.startsWith(realVaultPath)) {
|
|
398
398
|
return {
|
|
399
399
|
valid: false,
|
|
400
400
|
reason: "Symlink target is outside vault"
|
|
401
401
|
};
|
|
402
402
|
}
|
|
403
|
-
const relativePath =
|
|
403
|
+
const relativePath = path16.relative(realVaultPath, realPath);
|
|
404
404
|
if (isSensitivePath(relativePath)) {
|
|
405
405
|
return {
|
|
406
406
|
valid: false,
|
|
@@ -408,11 +408,11 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
408
408
|
};
|
|
409
409
|
}
|
|
410
410
|
} catch {
|
|
411
|
-
const parentDir =
|
|
411
|
+
const parentDir = path16.dirname(fullPath);
|
|
412
412
|
try {
|
|
413
|
-
await
|
|
414
|
-
const realParentPath = await
|
|
415
|
-
const realVaultPath = await
|
|
413
|
+
await fs17.access(parentDir);
|
|
414
|
+
const realParentPath = await fs17.realpath(parentDir);
|
|
415
|
+
const realVaultPath = await fs17.realpath(vaultPath2);
|
|
416
416
|
if (!realParentPath.startsWith(realVaultPath)) {
|
|
417
417
|
return {
|
|
418
418
|
valid: false,
|
|
@@ -434,8 +434,8 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
434
434
|
if (!validatePath(vaultPath2, notePath)) {
|
|
435
435
|
throw new Error("Invalid path: path traversal not allowed");
|
|
436
436
|
}
|
|
437
|
-
const fullPath =
|
|
438
|
-
const rawContent = await
|
|
437
|
+
const fullPath = path16.join(vaultPath2, notePath);
|
|
438
|
+
const rawContent = await fs17.readFile(fullPath, "utf-8");
|
|
439
439
|
const lineEnding = detectLineEnding(rawContent);
|
|
440
440
|
const normalizedContent = normalizeLineEndings(rawContent);
|
|
441
441
|
const parsed = matter5(normalizedContent);
|
|
@@ -483,11 +483,11 @@ async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEn
|
|
|
483
483
|
if (!validation.valid) {
|
|
484
484
|
throw new Error(`Invalid path: ${validation.reason}`);
|
|
485
485
|
}
|
|
486
|
-
const fullPath =
|
|
486
|
+
const fullPath = path16.join(vaultPath2, notePath);
|
|
487
487
|
let output = matter5.stringify(content, frontmatter);
|
|
488
488
|
output = normalizeTrailingNewline(output);
|
|
489
489
|
output = convertLineEndings(output, lineEnding);
|
|
490
|
-
await
|
|
490
|
+
await fs17.writeFile(fullPath, output, "utf-8");
|
|
491
491
|
}
|
|
492
492
|
function removeFromSection(content, section, pattern, mode = "first", useRegex = false) {
|
|
493
493
|
const lines = content.split("\n");
|
|
@@ -711,8 +711,8 @@ function createContext(variables = {}) {
|
|
|
711
711
|
}
|
|
712
712
|
};
|
|
713
713
|
}
|
|
714
|
-
function resolvePath(obj,
|
|
715
|
-
const parts =
|
|
714
|
+
function resolvePath(obj, path28) {
|
|
715
|
+
const parts = path28.split(".");
|
|
716
716
|
let current = obj;
|
|
717
717
|
for (const part of parts) {
|
|
718
718
|
if (current === void 0 || current === null) {
|
|
@@ -1150,8 +1150,8 @@ __export(conditions_exports, {
|
|
|
1150
1150
|
evaluateCondition: () => evaluateCondition,
|
|
1151
1151
|
shouldStepExecute: () => shouldStepExecute
|
|
1152
1152
|
});
|
|
1153
|
-
import
|
|
1154
|
-
import
|
|
1153
|
+
import fs23 from "fs/promises";
|
|
1154
|
+
import path22 from "path";
|
|
1155
1155
|
async function evaluateCondition(condition, vaultPath2, context) {
|
|
1156
1156
|
const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
|
|
1157
1157
|
const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
|
|
@@ -1204,9 +1204,9 @@ async function evaluateCondition(condition, vaultPath2, context) {
|
|
|
1204
1204
|
}
|
|
1205
1205
|
}
|
|
1206
1206
|
async function evaluateFileExists(vaultPath2, notePath, expectExists) {
|
|
1207
|
-
const fullPath =
|
|
1207
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
1208
1208
|
try {
|
|
1209
|
-
await
|
|
1209
|
+
await fs23.access(fullPath);
|
|
1210
1210
|
return {
|
|
1211
1211
|
met: expectExists,
|
|
1212
1212
|
reason: expectExists ? `File exists: ${notePath}` : `File exists (expected not to): ${notePath}`
|
|
@@ -1219,9 +1219,9 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
|
|
|
1219
1219
|
}
|
|
1220
1220
|
}
|
|
1221
1221
|
async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
|
|
1222
|
-
const fullPath =
|
|
1222
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
1223
1223
|
try {
|
|
1224
|
-
await
|
|
1224
|
+
await fs23.access(fullPath);
|
|
1225
1225
|
} catch {
|
|
1226
1226
|
return {
|
|
1227
1227
|
met: !expectExists,
|
|
@@ -1250,9 +1250,9 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
|
|
|
1250
1250
|
}
|
|
1251
1251
|
}
|
|
1252
1252
|
async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
|
|
1253
|
-
const fullPath =
|
|
1253
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
1254
1254
|
try {
|
|
1255
|
-
await
|
|
1255
|
+
await fs23.access(fullPath);
|
|
1256
1256
|
} catch {
|
|
1257
1257
|
return {
|
|
1258
1258
|
met: !expectExists,
|
|
@@ -1281,9 +1281,9 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
|
|
|
1281
1281
|
}
|
|
1282
1282
|
}
|
|
1283
1283
|
async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
|
|
1284
|
-
const fullPath =
|
|
1284
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
1285
1285
|
try {
|
|
1286
|
-
await
|
|
1286
|
+
await fs23.access(fullPath);
|
|
1287
1287
|
} catch {
|
|
1288
1288
|
return {
|
|
1289
1289
|
met: false,
|
|
@@ -1424,6 +1424,7 @@ var init_taskHelpers = __esm({
|
|
|
1424
1424
|
});
|
|
1425
1425
|
|
|
1426
1426
|
// src/index.ts
|
|
1427
|
+
import * as path27 from "path";
|
|
1427
1428
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1428
1429
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1429
1430
|
|
|
@@ -1689,8 +1690,8 @@ function updateIndexProgress(parsed, total) {
|
|
|
1689
1690
|
function normalizeTarget(target) {
|
|
1690
1691
|
return target.toLowerCase().replace(/\.md$/, "");
|
|
1691
1692
|
}
|
|
1692
|
-
function normalizeNotePath(
|
|
1693
|
-
return
|
|
1693
|
+
function normalizeNotePath(path28) {
|
|
1694
|
+
return path28.toLowerCase().replace(/\.md$/, "");
|
|
1694
1695
|
}
|
|
1695
1696
|
async function buildVaultIndex(vaultPath2, options = {}) {
|
|
1696
1697
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
|
|
@@ -1884,7 +1885,7 @@ function findSimilarEntity(index, target) {
|
|
|
1884
1885
|
}
|
|
1885
1886
|
const maxDist = normalizedLen <= 10 ? 1 : 2;
|
|
1886
1887
|
let bestMatch;
|
|
1887
|
-
for (const [entity,
|
|
1888
|
+
for (const [entity, path28] of index.entities) {
|
|
1888
1889
|
const lenDiff = Math.abs(entity.length - normalizedLen);
|
|
1889
1890
|
if (lenDiff > maxDist) {
|
|
1890
1891
|
continue;
|
|
@@ -1892,7 +1893,7 @@ function findSimilarEntity(index, target) {
|
|
|
1892
1893
|
const dist = levenshteinDistance(normalized, entity);
|
|
1893
1894
|
if (dist > 0 && dist <= maxDist) {
|
|
1894
1895
|
if (!bestMatch || dist < bestMatch.distance) {
|
|
1895
|
-
bestMatch = { path:
|
|
1896
|
+
bestMatch = { path: path28, entity, distance: dist };
|
|
1896
1897
|
if (dist === 1) {
|
|
1897
1898
|
return bestMatch;
|
|
1898
1899
|
}
|
|
@@ -2354,30 +2355,30 @@ var EventQueue = class {
|
|
|
2354
2355
|
* Add a new event to the queue
|
|
2355
2356
|
*/
|
|
2356
2357
|
push(type, rawPath) {
|
|
2357
|
-
const
|
|
2358
|
+
const path28 = normalizePath(rawPath);
|
|
2358
2359
|
const now = Date.now();
|
|
2359
2360
|
const event = {
|
|
2360
2361
|
type,
|
|
2361
|
-
path:
|
|
2362
|
+
path: path28,
|
|
2362
2363
|
timestamp: now
|
|
2363
2364
|
};
|
|
2364
|
-
let pending = this.pending.get(
|
|
2365
|
+
let pending = this.pending.get(path28);
|
|
2365
2366
|
if (!pending) {
|
|
2366
2367
|
pending = {
|
|
2367
2368
|
events: [],
|
|
2368
2369
|
timer: null,
|
|
2369
2370
|
lastEvent: now
|
|
2370
2371
|
};
|
|
2371
|
-
this.pending.set(
|
|
2372
|
+
this.pending.set(path28, pending);
|
|
2372
2373
|
}
|
|
2373
2374
|
pending.events.push(event);
|
|
2374
2375
|
pending.lastEvent = now;
|
|
2375
|
-
console.error(`[flywheel] QUEUE: pushed ${type} for ${
|
|
2376
|
+
console.error(`[flywheel] QUEUE: pushed ${type} for ${path28}, pending=${this.pending.size}`);
|
|
2376
2377
|
if (pending.timer) {
|
|
2377
2378
|
clearTimeout(pending.timer);
|
|
2378
2379
|
}
|
|
2379
2380
|
pending.timer = setTimeout(() => {
|
|
2380
|
-
this.flushPath(
|
|
2381
|
+
this.flushPath(path28);
|
|
2381
2382
|
}, this.config.debounceMs);
|
|
2382
2383
|
if (this.pending.size >= this.config.batchSize) {
|
|
2383
2384
|
this.flush();
|
|
@@ -2398,10 +2399,10 @@ var EventQueue = class {
|
|
|
2398
2399
|
/**
|
|
2399
2400
|
* Flush a single path's events
|
|
2400
2401
|
*/
|
|
2401
|
-
flushPath(
|
|
2402
|
-
const pending = this.pending.get(
|
|
2402
|
+
flushPath(path28) {
|
|
2403
|
+
const pending = this.pending.get(path28);
|
|
2403
2404
|
if (!pending || pending.events.length === 0) return;
|
|
2404
|
-
console.error(`[flywheel] QUEUE: flushing ${
|
|
2405
|
+
console.error(`[flywheel] QUEUE: flushing ${path28}, events=${pending.events.length}`);
|
|
2405
2406
|
if (pending.timer) {
|
|
2406
2407
|
clearTimeout(pending.timer);
|
|
2407
2408
|
pending.timer = null;
|
|
@@ -2410,7 +2411,7 @@ var EventQueue = class {
|
|
|
2410
2411
|
if (coalescedType) {
|
|
2411
2412
|
const coalesced = {
|
|
2412
2413
|
type: coalescedType,
|
|
2413
|
-
path:
|
|
2414
|
+
path: path28,
|
|
2414
2415
|
originalEvents: [...pending.events]
|
|
2415
2416
|
};
|
|
2416
2417
|
this.onBatch({
|
|
@@ -2418,7 +2419,7 @@ var EventQueue = class {
|
|
|
2418
2419
|
timestamp: Date.now()
|
|
2419
2420
|
});
|
|
2420
2421
|
}
|
|
2421
|
-
this.pending.delete(
|
|
2422
|
+
this.pending.delete(path28);
|
|
2422
2423
|
}
|
|
2423
2424
|
/**
|
|
2424
2425
|
* Flush all pending events
|
|
@@ -2430,7 +2431,7 @@ var EventQueue = class {
|
|
|
2430
2431
|
}
|
|
2431
2432
|
if (this.pending.size === 0) return;
|
|
2432
2433
|
const events = [];
|
|
2433
|
-
for (const [
|
|
2434
|
+
for (const [path28, pending] of this.pending) {
|
|
2434
2435
|
if (pending.timer) {
|
|
2435
2436
|
clearTimeout(pending.timer);
|
|
2436
2437
|
}
|
|
@@ -2438,7 +2439,7 @@ var EventQueue = class {
|
|
|
2438
2439
|
if (coalescedType) {
|
|
2439
2440
|
events.push({
|
|
2440
2441
|
type: coalescedType,
|
|
2441
|
-
path:
|
|
2442
|
+
path: path28,
|
|
2442
2443
|
originalEvents: [...pending.events]
|
|
2443
2444
|
});
|
|
2444
2445
|
}
|
|
@@ -2587,31 +2588,31 @@ function createVaultWatcher(options) {
|
|
|
2587
2588
|
usePolling: config.usePolling,
|
|
2588
2589
|
interval: config.usePolling ? config.pollInterval : void 0
|
|
2589
2590
|
});
|
|
2590
|
-
watcher.on("add", (
|
|
2591
|
-
console.error(`[flywheel] RAW EVENT: add ${
|
|
2592
|
-
if (shouldWatch(
|
|
2593
|
-
console.error(`[flywheel] ACCEPTED: add ${
|
|
2594
|
-
eventQueue.push("add",
|
|
2591
|
+
watcher.on("add", (path28) => {
|
|
2592
|
+
console.error(`[flywheel] RAW EVENT: add ${path28}`);
|
|
2593
|
+
if (shouldWatch(path28, vaultPath2)) {
|
|
2594
|
+
console.error(`[flywheel] ACCEPTED: add ${path28}`);
|
|
2595
|
+
eventQueue.push("add", path28);
|
|
2595
2596
|
} else {
|
|
2596
|
-
console.error(`[flywheel] FILTERED: add ${
|
|
2597
|
+
console.error(`[flywheel] FILTERED: add ${path28}`);
|
|
2597
2598
|
}
|
|
2598
2599
|
});
|
|
2599
|
-
watcher.on("change", (
|
|
2600
|
-
console.error(`[flywheel] RAW EVENT: change ${
|
|
2601
|
-
if (shouldWatch(
|
|
2602
|
-
console.error(`[flywheel] ACCEPTED: change ${
|
|
2603
|
-
eventQueue.push("change",
|
|
2600
|
+
watcher.on("change", (path28) => {
|
|
2601
|
+
console.error(`[flywheel] RAW EVENT: change ${path28}`);
|
|
2602
|
+
if (shouldWatch(path28, vaultPath2)) {
|
|
2603
|
+
console.error(`[flywheel] ACCEPTED: change ${path28}`);
|
|
2604
|
+
eventQueue.push("change", path28);
|
|
2604
2605
|
} else {
|
|
2605
|
-
console.error(`[flywheel] FILTERED: change ${
|
|
2606
|
+
console.error(`[flywheel] FILTERED: change ${path28}`);
|
|
2606
2607
|
}
|
|
2607
2608
|
});
|
|
2608
|
-
watcher.on("unlink", (
|
|
2609
|
-
console.error(`[flywheel] RAW EVENT: unlink ${
|
|
2610
|
-
if (shouldWatch(
|
|
2611
|
-
console.error(`[flywheel] ACCEPTED: unlink ${
|
|
2612
|
-
eventQueue.push("unlink",
|
|
2609
|
+
watcher.on("unlink", (path28) => {
|
|
2610
|
+
console.error(`[flywheel] RAW EVENT: unlink ${path28}`);
|
|
2611
|
+
if (shouldWatch(path28, vaultPath2)) {
|
|
2612
|
+
console.error(`[flywheel] ACCEPTED: unlink ${path28}`);
|
|
2613
|
+
eventQueue.push("unlink", path28);
|
|
2613
2614
|
} else {
|
|
2614
|
-
console.error(`[flywheel] FILTERED: unlink ${
|
|
2615
|
+
console.error(`[flywheel] FILTERED: unlink ${path28}`);
|
|
2615
2616
|
}
|
|
2616
2617
|
});
|
|
2617
2618
|
watcher.on("ready", () => {
|
|
@@ -2901,10 +2902,10 @@ function getAllFeedbackBoosts(stateDb2, folder) {
|
|
|
2901
2902
|
for (const row of globalRows) {
|
|
2902
2903
|
let accuracy;
|
|
2903
2904
|
let sampleCount;
|
|
2904
|
-
const
|
|
2905
|
-
if (
|
|
2906
|
-
accuracy =
|
|
2907
|
-
sampleCount =
|
|
2905
|
+
const fs28 = folderStats?.get(row.entity);
|
|
2906
|
+
if (fs28 && fs28.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
|
|
2907
|
+
accuracy = fs28.accuracy;
|
|
2908
|
+
sampleCount = fs28.count;
|
|
2908
2909
|
} else {
|
|
2909
2910
|
accuracy = row.correct_count / row.total;
|
|
2910
2911
|
sampleCount = row.total;
|
|
@@ -5271,11 +5272,228 @@ function getFTS5State() {
|
|
|
5271
5272
|
return { ...state };
|
|
5272
5273
|
}
|
|
5273
5274
|
|
|
5275
|
+
// src/core/read/embeddings.ts
|
|
5276
|
+
import * as crypto from "crypto";
|
|
5277
|
+
import * as fs7 from "fs";
|
|
5278
|
+
var MODEL_ID = "Xenova/all-MiniLM-L6-v2";
|
|
5279
|
+
var EXCLUDED_DIRS3 = /* @__PURE__ */ new Set([
|
|
5280
|
+
".obsidian",
|
|
5281
|
+
".trash",
|
|
5282
|
+
".git",
|
|
5283
|
+
"node_modules",
|
|
5284
|
+
"templates",
|
|
5285
|
+
".claude",
|
|
5286
|
+
".flywheel"
|
|
5287
|
+
]);
|
|
5288
|
+
var MAX_FILE_SIZE2 = 5 * 1024 * 1024;
|
|
5289
|
+
var db2 = null;
|
|
5290
|
+
var pipeline = null;
|
|
5291
|
+
var initPromise = null;
|
|
5292
|
+
function setEmbeddingsDatabase(database) {
|
|
5293
|
+
db2 = database;
|
|
5294
|
+
}
|
|
5295
|
+
async function initEmbeddings() {
|
|
5296
|
+
if (pipeline) return;
|
|
5297
|
+
if (initPromise) return initPromise;
|
|
5298
|
+
initPromise = (async () => {
|
|
5299
|
+
try {
|
|
5300
|
+
const transformers = await Function("specifier", "return import(specifier)")("@huggingface/transformers");
|
|
5301
|
+
pipeline = await transformers.pipeline("feature-extraction", MODEL_ID, {
|
|
5302
|
+
dtype: "fp32"
|
|
5303
|
+
});
|
|
5304
|
+
} catch (err) {
|
|
5305
|
+
initPromise = null;
|
|
5306
|
+
if (err instanceof Error && (err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Cannot find module") || err.message.includes("ERR_MODULE_NOT_FOUND"))) {
|
|
5307
|
+
throw new Error(
|
|
5308
|
+
"Semantic search requires @huggingface/transformers. Install it with: npm install @huggingface/transformers"
|
|
5309
|
+
);
|
|
5310
|
+
}
|
|
5311
|
+
throw err;
|
|
5312
|
+
}
|
|
5313
|
+
})();
|
|
5314
|
+
return initPromise;
|
|
5315
|
+
}
|
|
5316
|
+
async function embedText(text) {
|
|
5317
|
+
await initEmbeddings();
|
|
5318
|
+
const truncated = text.slice(0, 2e3);
|
|
5319
|
+
const result = await pipeline(truncated, { pooling: "mean", normalize: true });
|
|
5320
|
+
return new Float32Array(result.data);
|
|
5321
|
+
}
|
|
5322
|
+
function contentHash(content) {
|
|
5323
|
+
return crypto.createHash("md5").update(content).digest("hex");
|
|
5324
|
+
}
|
|
5325
|
+
function shouldIndexFile2(filePath) {
|
|
5326
|
+
const parts = filePath.split("/");
|
|
5327
|
+
return !parts.some((part) => EXCLUDED_DIRS3.has(part));
|
|
5328
|
+
}
|
|
5329
|
+
async function buildEmbeddingsIndex(vaultPath2, onProgress) {
|
|
5330
|
+
if (!db2) {
|
|
5331
|
+
throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
|
|
5332
|
+
}
|
|
5333
|
+
await initEmbeddings();
|
|
5334
|
+
const files = await scanVault(vaultPath2);
|
|
5335
|
+
const indexable = files.filter((f) => shouldIndexFile2(f.path));
|
|
5336
|
+
const existingHashes = /* @__PURE__ */ new Map();
|
|
5337
|
+
const rows = db2.prepare("SELECT path, content_hash FROM note_embeddings").all();
|
|
5338
|
+
for (const row of rows) {
|
|
5339
|
+
existingHashes.set(row.path, row.content_hash);
|
|
5340
|
+
}
|
|
5341
|
+
const upsert = db2.prepare(`
|
|
5342
|
+
INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
|
|
5343
|
+
VALUES (?, ?, ?, ?, ?)
|
|
5344
|
+
`);
|
|
5345
|
+
const progress = { total: indexable.length, current: 0, skipped: 0 };
|
|
5346
|
+
for (const file of indexable) {
|
|
5347
|
+
progress.current++;
|
|
5348
|
+
try {
|
|
5349
|
+
const stats = fs7.statSync(file.absolutePath);
|
|
5350
|
+
if (stats.size > MAX_FILE_SIZE2) {
|
|
5351
|
+
progress.skipped++;
|
|
5352
|
+
continue;
|
|
5353
|
+
}
|
|
5354
|
+
const content = fs7.readFileSync(file.absolutePath, "utf-8");
|
|
5355
|
+
const hash = contentHash(content);
|
|
5356
|
+
if (existingHashes.get(file.path) === hash) {
|
|
5357
|
+
progress.skipped++;
|
|
5358
|
+
if (onProgress) onProgress(progress);
|
|
5359
|
+
continue;
|
|
5360
|
+
}
|
|
5361
|
+
const embedding = await embedText(content);
|
|
5362
|
+
const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
5363
|
+
upsert.run(file.path, buf, hash, MODEL_ID, Date.now());
|
|
5364
|
+
} catch {
|
|
5365
|
+
progress.skipped++;
|
|
5366
|
+
}
|
|
5367
|
+
if (onProgress) onProgress(progress);
|
|
5368
|
+
}
|
|
5369
|
+
const currentPaths = new Set(indexable.map((f) => f.path));
|
|
5370
|
+
const deleteStmt = db2.prepare("DELETE FROM note_embeddings WHERE path = ?");
|
|
5371
|
+
for (const existingPath of existingHashes.keys()) {
|
|
5372
|
+
if (!currentPaths.has(existingPath)) {
|
|
5373
|
+
deleteStmt.run(existingPath);
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
console.error(`[Semantic] Indexed ${progress.current - progress.skipped} notes, skipped ${progress.skipped}`);
|
|
5377
|
+
return progress;
|
|
5378
|
+
}
|
|
5379
|
+
async function updateEmbedding(notePath, absolutePath) {
|
|
5380
|
+
if (!db2) return;
|
|
5381
|
+
try {
|
|
5382
|
+
const content = fs7.readFileSync(absolutePath, "utf-8");
|
|
5383
|
+
const hash = contentHash(content);
|
|
5384
|
+
const existing = db2.prepare("SELECT content_hash FROM note_embeddings WHERE path = ?").get(notePath);
|
|
5385
|
+
if (existing?.content_hash === hash) return;
|
|
5386
|
+
const embedding = await embedText(content);
|
|
5387
|
+
const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
5388
|
+
db2.prepare(`
|
|
5389
|
+
INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
|
|
5390
|
+
VALUES (?, ?, ?, ?, ?)
|
|
5391
|
+
`).run(notePath, buf, hash, MODEL_ID, Date.now());
|
|
5392
|
+
} catch {
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
5395
|
+
function removeEmbedding(notePath) {
|
|
5396
|
+
if (!db2) return;
|
|
5397
|
+
db2.prepare("DELETE FROM note_embeddings WHERE path = ?").run(notePath);
|
|
5398
|
+
}
|
|
5399
|
+
function cosineSimilarity(a, b) {
|
|
5400
|
+
let dot = 0;
|
|
5401
|
+
let normA = 0;
|
|
5402
|
+
let normB = 0;
|
|
5403
|
+
for (let i = 0; i < a.length; i++) {
|
|
5404
|
+
dot += a[i] * b[i];
|
|
5405
|
+
normA += a[i] * a[i];
|
|
5406
|
+
normB += b[i] * b[i];
|
|
5407
|
+
}
|
|
5408
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
5409
|
+
if (denom === 0) return 0;
|
|
5410
|
+
return dot / denom;
|
|
5411
|
+
}
|
|
5412
|
+
async function semanticSearch(query, limit = 10) {
|
|
5413
|
+
if (!db2) {
|
|
5414
|
+
throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
|
|
5415
|
+
}
|
|
5416
|
+
const queryEmbedding = await embedText(query);
|
|
5417
|
+
const rows = db2.prepare("SELECT path, embedding FROM note_embeddings").all();
|
|
5418
|
+
const scored = [];
|
|
5419
|
+
for (const row of rows) {
|
|
5420
|
+
const noteEmbedding = new Float32Array(
|
|
5421
|
+
row.embedding.buffer,
|
|
5422
|
+
row.embedding.byteOffset,
|
|
5423
|
+
row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
|
|
5424
|
+
);
|
|
5425
|
+
const score = cosineSimilarity(queryEmbedding, noteEmbedding);
|
|
5426
|
+
const title = row.path.replace(/\.md$/, "").split("/").pop() || row.path;
|
|
5427
|
+
scored.push({ path: row.path, title, score: Math.round(score * 1e3) / 1e3 });
|
|
5428
|
+
}
|
|
5429
|
+
scored.sort((a, b) => b.score - a.score);
|
|
5430
|
+
return scored.slice(0, limit);
|
|
5431
|
+
}
|
|
5432
|
+
async function findSemanticallySimilar(sourcePath, limit = 10, excludePaths) {
|
|
5433
|
+
if (!db2) {
|
|
5434
|
+
throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
|
|
5435
|
+
}
|
|
5436
|
+
const sourceRow = db2.prepare("SELECT embedding FROM note_embeddings WHERE path = ?").get(sourcePath);
|
|
5437
|
+
if (!sourceRow) {
|
|
5438
|
+
return [];
|
|
5439
|
+
}
|
|
5440
|
+
const sourceEmbedding = new Float32Array(
|
|
5441
|
+
sourceRow.embedding.buffer,
|
|
5442
|
+
sourceRow.embedding.byteOffset,
|
|
5443
|
+
sourceRow.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
|
|
5444
|
+
);
|
|
5445
|
+
const rows = db2.prepare("SELECT path, embedding FROM note_embeddings WHERE path != ?").all(sourcePath);
|
|
5446
|
+
const scored = [];
|
|
5447
|
+
for (const row of rows) {
|
|
5448
|
+
if (excludePaths?.has(row.path)) continue;
|
|
5449
|
+
const noteEmbedding = new Float32Array(
|
|
5450
|
+
row.embedding.buffer,
|
|
5451
|
+
row.embedding.byteOffset,
|
|
5452
|
+
row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
|
|
5453
|
+
);
|
|
5454
|
+
const score = cosineSimilarity(sourceEmbedding, noteEmbedding);
|
|
5455
|
+
const title = row.path.replace(/\.md$/, "").split("/").pop() || row.path;
|
|
5456
|
+
scored.push({ path: row.path, title, score: Math.round(score * 1e3) / 1e3 });
|
|
5457
|
+
}
|
|
5458
|
+
scored.sort((a, b) => b.score - a.score);
|
|
5459
|
+
return scored.slice(0, limit);
|
|
5460
|
+
}
|
|
5461
|
+
function reciprocalRankFusion(...lists) {
|
|
5462
|
+
const k = 60;
|
|
5463
|
+
const scores = /* @__PURE__ */ new Map();
|
|
5464
|
+
for (const list of lists) {
|
|
5465
|
+
for (let rank = 0; rank < list.length; rank++) {
|
|
5466
|
+
const item = list[rank];
|
|
5467
|
+
const rrfScore = 1 / (k + rank + 1);
|
|
5468
|
+
scores.set(item.path, (scores.get(item.path) || 0) + rrfScore);
|
|
5469
|
+
}
|
|
5470
|
+
}
|
|
5471
|
+
return scores;
|
|
5472
|
+
}
|
|
5473
|
+
function hasEmbeddingsIndex() {
|
|
5474
|
+
if (!db2) return false;
|
|
5475
|
+
try {
|
|
5476
|
+
const row = db2.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
|
|
5477
|
+
return row.count > 0;
|
|
5478
|
+
} catch {
|
|
5479
|
+
return false;
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
function getEmbeddingsCount() {
|
|
5483
|
+
if (!db2) return 0;
|
|
5484
|
+
try {
|
|
5485
|
+
const row = db2.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
|
|
5486
|
+
return row.count;
|
|
5487
|
+
} catch {
|
|
5488
|
+
return 0;
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5274
5492
|
// src/index.ts
|
|
5275
|
-
import { openStateDb, scanVaultEntities as scanVaultEntities3 } from "@velvetmonkey/vault-core";
|
|
5493
|
+
import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId } from "@velvetmonkey/vault-core";
|
|
5276
5494
|
|
|
5277
5495
|
// src/tools/read/graph.ts
|
|
5278
|
-
import * as
|
|
5496
|
+
import * as fs8 from "fs";
|
|
5279
5497
|
import * as path8 from "path";
|
|
5280
5498
|
import { z } from "zod";
|
|
5281
5499
|
|
|
@@ -5561,7 +5779,7 @@ function requireIndex() {
|
|
|
5561
5779
|
async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
|
|
5562
5780
|
try {
|
|
5563
5781
|
const fullPath = path8.join(vaultPath2, sourcePath);
|
|
5564
|
-
const content = await
|
|
5782
|
+
const content = await fs8.promises.readFile(fullPath, "utf-8");
|
|
5565
5783
|
const lines = content.split("\n");
|
|
5566
5784
|
const startLine = Math.max(0, line - 1 - contextLines);
|
|
5567
5785
|
const endLine = Math.min(lines.length, line + contextLines);
|
|
@@ -5864,14 +6082,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
5864
6082
|
};
|
|
5865
6083
|
function findSimilarEntity2(target, entities) {
|
|
5866
6084
|
const targetLower = target.toLowerCase();
|
|
5867
|
-
for (const [name,
|
|
6085
|
+
for (const [name, path28] of entities) {
|
|
5868
6086
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
5869
|
-
return
|
|
6087
|
+
return path28;
|
|
5870
6088
|
}
|
|
5871
6089
|
}
|
|
5872
|
-
for (const [name,
|
|
6090
|
+
for (const [name, path28] of entities) {
|
|
5873
6091
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
5874
|
-
return
|
|
6092
|
+
return path28;
|
|
5875
6093
|
}
|
|
5876
6094
|
}
|
|
5877
6095
|
return void 0;
|
|
@@ -5953,7 +6171,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
5953
6171
|
}
|
|
5954
6172
|
|
|
5955
6173
|
// src/tools/read/health.ts
|
|
5956
|
-
import * as
|
|
6174
|
+
import * as fs9 from "fs";
|
|
5957
6175
|
import { z as z3 } from "zod";
|
|
5958
6176
|
|
|
5959
6177
|
// src/tools/read/periodic.ts
|
|
@@ -6186,9 +6404,84 @@ function getActivitySummary(index, days) {
|
|
|
6186
6404
|
};
|
|
6187
6405
|
}
|
|
6188
6406
|
|
|
6407
|
+
// src/core/shared/indexActivity.ts
|
|
6408
|
+
function recordIndexEvent(stateDb2, event) {
|
|
6409
|
+
stateDb2.db.prepare(
|
|
6410
|
+
`INSERT INTO index_events (timestamp, trigger, duration_ms, success, note_count, files_changed, changed_paths, error)
|
|
6411
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
6412
|
+
).run(
|
|
6413
|
+
Date.now(),
|
|
6414
|
+
event.trigger,
|
|
6415
|
+
event.duration_ms,
|
|
6416
|
+
event.success !== false ? 1 : 0,
|
|
6417
|
+
event.note_count ?? null,
|
|
6418
|
+
event.files_changed ?? null,
|
|
6419
|
+
event.changed_paths ? JSON.stringify(event.changed_paths) : null,
|
|
6420
|
+
event.error ?? null
|
|
6421
|
+
);
|
|
6422
|
+
}
|
|
6423
|
+
function rowToEvent(row) {
|
|
6424
|
+
return {
|
|
6425
|
+
id: row.id,
|
|
6426
|
+
timestamp: row.timestamp,
|
|
6427
|
+
trigger: row.trigger,
|
|
6428
|
+
duration_ms: row.duration_ms,
|
|
6429
|
+
success: row.success === 1,
|
|
6430
|
+
note_count: row.note_count,
|
|
6431
|
+
files_changed: row.files_changed,
|
|
6432
|
+
changed_paths: row.changed_paths ? JSON.parse(row.changed_paths) : null,
|
|
6433
|
+
error: row.error
|
|
6434
|
+
};
|
|
6435
|
+
}
|
|
6436
|
+
function getRecentIndexEvents(stateDb2, limit = 20) {
|
|
6437
|
+
const rows = stateDb2.db.prepare(
|
|
6438
|
+
"SELECT * FROM index_events ORDER BY timestamp DESC LIMIT ?"
|
|
6439
|
+
).all(limit);
|
|
6440
|
+
return rows.map(rowToEvent);
|
|
6441
|
+
}
|
|
6442
|
+
function getIndexActivitySummary(stateDb2) {
|
|
6443
|
+
const now = Date.now();
|
|
6444
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
6445
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
6446
|
+
const last24h = now - 24 * 60 * 60 * 1e3;
|
|
6447
|
+
const totalRow = stateDb2.db.prepare(
|
|
6448
|
+
"SELECT COUNT(*) as count FROM index_events"
|
|
6449
|
+
).get();
|
|
6450
|
+
const todayRow = stateDb2.db.prepare(
|
|
6451
|
+
"SELECT COUNT(*) as count FROM index_events WHERE timestamp >= ?"
|
|
6452
|
+
).get(todayStart.getTime());
|
|
6453
|
+
const last24hRow = stateDb2.db.prepare(
|
|
6454
|
+
"SELECT COUNT(*) as count FROM index_events WHERE timestamp >= ?"
|
|
6455
|
+
).get(last24h);
|
|
6456
|
+
const avgRow = stateDb2.db.prepare(
|
|
6457
|
+
"SELECT AVG(duration_ms) as avg_ms FROM index_events WHERE success = 1"
|
|
6458
|
+
).get();
|
|
6459
|
+
const failureRow = stateDb2.db.prepare(
|
|
6460
|
+
"SELECT COUNT(*) as count FROM index_events WHERE success = 0"
|
|
6461
|
+
).get();
|
|
6462
|
+
const lastRow = stateDb2.db.prepare(
|
|
6463
|
+
"SELECT * FROM index_events ORDER BY timestamp DESC LIMIT 1"
|
|
6464
|
+
).get();
|
|
6465
|
+
return {
|
|
6466
|
+
total_rebuilds: totalRow.count,
|
|
6467
|
+
last_rebuild: lastRow ? rowToEvent(lastRow) : null,
|
|
6468
|
+
rebuilds_today: todayRow.count,
|
|
6469
|
+
rebuilds_last_24h: last24hRow.count,
|
|
6470
|
+
avg_duration_ms: Math.round(avgRow.avg_ms ?? 0),
|
|
6471
|
+
failure_count: failureRow.count
|
|
6472
|
+
};
|
|
6473
|
+
}
|
|
6474
|
+
function purgeOldIndexEvents(stateDb2, retentionDays = 90) {
|
|
6475
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
6476
|
+
const result = stateDb2.db.prepare(
|
|
6477
|
+
"DELETE FROM index_events WHERE timestamp < ?"
|
|
6478
|
+
).run(cutoff);
|
|
6479
|
+
return result.changes;
|
|
6480
|
+
}
|
|
6481
|
+
|
|
6189
6482
|
// src/tools/read/health.ts
|
|
6190
6483
|
var STALE_THRESHOLD_SECONDS = 300;
|
|
6191
|
-
function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({})) {
|
|
6484
|
+
function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({}), getStateDb = () => null) {
|
|
6192
6485
|
const IndexProgressSchema = z3.object({
|
|
6193
6486
|
parsed: z3.coerce.number().describe("Number of files parsed so far"),
|
|
6194
6487
|
total: z3.coerce.number().describe("Total number of files to parse")
|
|
@@ -6216,6 +6509,12 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
6216
6509
|
tag_count: z3.coerce.number().describe("Number of unique tags"),
|
|
6217
6510
|
periodic_notes: z3.array(PeriodicNoteInfoSchema).optional().describe("Detected periodic note conventions"),
|
|
6218
6511
|
config: z3.record(z3.unknown()).optional().describe("Current flywheel config (paths, templates, etc.)"),
|
|
6512
|
+
last_rebuild: z3.object({
|
|
6513
|
+
trigger: z3.string(),
|
|
6514
|
+
timestamp: z3.number(),
|
|
6515
|
+
duration_ms: z3.number(),
|
|
6516
|
+
ago_seconds: z3.number()
|
|
6517
|
+
}).optional().describe("Most recent index rebuild event"),
|
|
6219
6518
|
recommendations: z3.array(z3.string()).describe("Suggested actions if any issues detected")
|
|
6220
6519
|
};
|
|
6221
6520
|
server2.registerTool(
|
|
@@ -6235,7 +6534,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
6235
6534
|
const indexErrorObj = getIndexError();
|
|
6236
6535
|
let vaultAccessible = false;
|
|
6237
6536
|
try {
|
|
6238
|
-
|
|
6537
|
+
fs9.accessSync(vaultPath2, fs9.constants.R_OK);
|
|
6239
6538
|
vaultAccessible = true;
|
|
6240
6539
|
} catch {
|
|
6241
6540
|
vaultAccessible = false;
|
|
@@ -6284,6 +6583,23 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
6284
6583
|
}
|
|
6285
6584
|
const config = getConfig();
|
|
6286
6585
|
const configInfo = Object.keys(config).length > 0 ? config : void 0;
|
|
6586
|
+
let lastRebuild;
|
|
6587
|
+
const stateDb2 = getStateDb();
|
|
6588
|
+
if (stateDb2) {
|
|
6589
|
+
try {
|
|
6590
|
+
const events = getRecentIndexEvents(stateDb2, 1);
|
|
6591
|
+
if (events.length > 0) {
|
|
6592
|
+
const event = events[0];
|
|
6593
|
+
lastRebuild = {
|
|
6594
|
+
trigger: event.trigger,
|
|
6595
|
+
timestamp: event.timestamp,
|
|
6596
|
+
duration_ms: event.duration_ms,
|
|
6597
|
+
ago_seconds: Math.floor((Date.now() - event.timestamp) / 1e3)
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
} catch {
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6287
6603
|
const output = {
|
|
6288
6604
|
status,
|
|
6289
6605
|
vault_accessible: vaultAccessible,
|
|
@@ -6299,6 +6615,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
6299
6615
|
tag_count: tagCount,
|
|
6300
6616
|
periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
|
|
6301
6617
|
config: configInfo,
|
|
6618
|
+
last_rebuild: lastRebuild,
|
|
6302
6619
|
recommendations
|
|
6303
6620
|
};
|
|
6304
6621
|
return {
|
|
@@ -6348,8 +6665,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
6348
6665
|
daily_counts: z3.record(z3.number())
|
|
6349
6666
|
}).describe("Activity summary for the last 7 days")
|
|
6350
6667
|
};
|
|
6351
|
-
function isPeriodicNote(
|
|
6352
|
-
const filename =
|
|
6668
|
+
function isPeriodicNote(path28) {
|
|
6669
|
+
const filename = path28.split("/").pop() || "";
|
|
6353
6670
|
const nameWithoutExt = filename.replace(/\.md$/, "");
|
|
6354
6671
|
const patterns = [
|
|
6355
6672
|
/^\d{4}-\d{2}-\d{2}$/,
|
|
@@ -6364,7 +6681,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
6364
6681
|
// YYYY (yearly)
|
|
6365
6682
|
];
|
|
6366
6683
|
const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
|
|
6367
|
-
const folder =
|
|
6684
|
+
const folder = path28.split("/")[0]?.toLowerCase() || "";
|
|
6368
6685
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
6369
6686
|
}
|
|
6370
6687
|
server2.registerTool(
|
|
@@ -6533,10 +6850,10 @@ function sortNotes(notes, sortBy, order) {
|
|
|
6533
6850
|
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
6534
6851
|
server2.tool(
|
|
6535
6852
|
"search",
|
|
6536
|
-
'Search the vault across metadata, content, and entities. Scope controls what to search: "metadata" for frontmatter/tags/folders, "content" for full-text search (FTS5), "entities" for people/projects/technologies, "all" (default) tries metadata then falls back to content search.\n\nExample: search({ query: "quarterly review", scope: "content", limit: 5 })\nExample: search({ where: { type: "project", status: "active" }, scope: "metadata" })',
|
|
6853
|
+
'Search the vault across metadata, content, and entities. Scope controls what to search: "metadata" for frontmatter/tags/folders, "content" for full-text search (FTS5), "entities" for people/projects/technologies, "all" (default) tries metadata then falls back to content search. When embeddings have been built (via init_semantic), content and all scopes automatically include embedding-based results via hybrid ranking.\n\nExample: search({ query: "quarterly review", scope: "content", limit: 5 })\nExample: search({ where: { type: "project", status: "active" }, scope: "metadata" })',
|
|
6537
6854
|
{
|
|
6538
6855
|
query: z4.string().optional().describe('Search query text. Required for scope "content", "entities", "all". For "metadata" scope, use filters instead.'),
|
|
6539
|
-
scope: z4.enum(["metadata", "content", "entities", "all"]).default("all").describe("What to search: metadata (frontmatter/tags/folders), content (FTS5 full-text), entities (people/projects), all (metadata then content)"),
|
|
6856
|
+
scope: z4.enum(["metadata", "content", "entities", "all"]).default("all").describe("What to search: metadata (frontmatter/tags/folders), content (FTS5 full-text), entities (people/projects), all (metadata then content). Semantic results are automatically included when embeddings have been built (via init_semantic)."),
|
|
6540
6857
|
// Metadata filters (used with scope "metadata" or "all")
|
|
6541
6858
|
where: z4.record(z4.unknown()).optional().describe('Frontmatter filters as key-value pairs. Example: { "type": "project", "status": "active" }'),
|
|
6542
6859
|
has_tag: z4.string().optional().describe("Filter to notes with this tag"),
|
|
@@ -6645,12 +6962,42 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6645
6962
|
console.error("[FTS5] Index stale or missing, rebuilding...");
|
|
6646
6963
|
await buildFTS5Index(vaultPath2);
|
|
6647
6964
|
}
|
|
6648
|
-
const
|
|
6965
|
+
const fts5Results = searchFTS5(vaultPath2, query, limit);
|
|
6966
|
+
if (hasEmbeddingsIndex()) {
|
|
6967
|
+
try {
|
|
6968
|
+
const semanticResults = await semanticSearch(query, limit);
|
|
6969
|
+
const fts5Ranked = fts5Results.map((r) => ({ path: r.path, title: r.title, snippet: r.snippet }));
|
|
6970
|
+
const semanticRanked = semanticResults.map((r) => ({ path: r.path, title: r.title }));
|
|
6971
|
+
const rrfScores = reciprocalRankFusion(fts5Ranked, semanticRanked);
|
|
6972
|
+
const allPaths = /* @__PURE__ */ new Set([...fts5Results.map((r) => r.path), ...semanticResults.map((r) => r.path)]);
|
|
6973
|
+
const fts5Map = new Map(fts5Results.map((r) => [r.path, r]));
|
|
6974
|
+
const semanticMap = new Map(semanticResults.map((r) => [r.path, r]));
|
|
6975
|
+
const merged = Array.from(allPaths).map((p) => ({
|
|
6976
|
+
path: p,
|
|
6977
|
+
title: fts5Map.get(p)?.title || semanticMap.get(p)?.title || p.replace(/\.md$/, "").split("/").pop() || p,
|
|
6978
|
+
snippet: fts5Map.get(p)?.snippet,
|
|
6979
|
+
rrf_score: Math.round((rrfScores.get(p) || 0) * 1e4) / 1e4,
|
|
6980
|
+
in_fts5: fts5Map.has(p),
|
|
6981
|
+
in_semantic: semanticMap.has(p)
|
|
6982
|
+
}));
|
|
6983
|
+
merged.sort((a, b) => b.rrf_score - a.rrf_score);
|
|
6984
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
6985
|
+
scope,
|
|
6986
|
+
method: "hybrid",
|
|
6987
|
+
query,
|
|
6988
|
+
total_results: Math.min(merged.length, limit),
|
|
6989
|
+
results: merged.slice(0, limit)
|
|
6990
|
+
}, null, 2) }] };
|
|
6991
|
+
} catch (err) {
|
|
6992
|
+
console.error("[Semantic] Hybrid search failed, falling back to FTS5:", err instanceof Error ? err.message : err);
|
|
6993
|
+
}
|
|
6994
|
+
}
|
|
6649
6995
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
6650
6996
|
scope: "content",
|
|
6997
|
+
method: "fts5",
|
|
6651
6998
|
query,
|
|
6652
|
-
total_results:
|
|
6653
|
-
results
|
|
6999
|
+
total_results: fts5Results.length,
|
|
7000
|
+
results: fts5Results
|
|
6654
7001
|
}, null, 2) }] };
|
|
6655
7002
|
}
|
|
6656
7003
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Invalid scope" }, null, 2) }] };
|
|
@@ -6659,7 +7006,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6659
7006
|
}
|
|
6660
7007
|
|
|
6661
7008
|
// src/tools/read/system.ts
|
|
6662
|
-
import * as
|
|
7009
|
+
import * as fs10 from "fs";
|
|
6663
7010
|
import * as path9 from "path";
|
|
6664
7011
|
import { z as z5 } from "zod";
|
|
6665
7012
|
import { scanVaultEntities as scanVaultEntities2 } from "@velvetmonkey/vault-core";
|
|
@@ -6736,12 +7083,20 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
6736
7083
|
} catch (err) {
|
|
6737
7084
|
console.error("[Flywheel] FTS5 rebuild failed:", err);
|
|
6738
7085
|
}
|
|
7086
|
+
const duration = Date.now() - startTime;
|
|
7087
|
+
if (stateDb2) {
|
|
7088
|
+
recordIndexEvent(stateDb2, {
|
|
7089
|
+
trigger: "manual_refresh",
|
|
7090
|
+
duration_ms: duration,
|
|
7091
|
+
note_count: newIndex.notes.size
|
|
7092
|
+
});
|
|
7093
|
+
}
|
|
6739
7094
|
const output = {
|
|
6740
7095
|
success: true,
|
|
6741
7096
|
notes_count: newIndex.notes.size,
|
|
6742
7097
|
entities_count: newIndex.entities.size,
|
|
6743
7098
|
fts5_notes: fts5Notes,
|
|
6744
|
-
duration_ms:
|
|
7099
|
+
duration_ms: duration
|
|
6745
7100
|
};
|
|
6746
7101
|
return {
|
|
6747
7102
|
content: [
|
|
@@ -6755,12 +7110,22 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
6755
7110
|
} catch (err) {
|
|
6756
7111
|
setIndexState("error");
|
|
6757
7112
|
setIndexError(err instanceof Error ? err : new Error(String(err)));
|
|
7113
|
+
const duration = Date.now() - startTime;
|
|
7114
|
+
const stateDb2 = getStateDb?.();
|
|
7115
|
+
if (stateDb2) {
|
|
7116
|
+
recordIndexEvent(stateDb2, {
|
|
7117
|
+
trigger: "manual_refresh",
|
|
7118
|
+
duration_ms: duration,
|
|
7119
|
+
success: false,
|
|
7120
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7121
|
+
});
|
|
7122
|
+
}
|
|
6758
7123
|
const output = {
|
|
6759
7124
|
success: false,
|
|
6760
7125
|
notes_count: 0,
|
|
6761
7126
|
entities_count: 0,
|
|
6762
7127
|
fts5_notes: 0,
|
|
6763
|
-
duration_ms:
|
|
7128
|
+
duration_ms: duration
|
|
6764
7129
|
};
|
|
6765
7130
|
return {
|
|
6766
7131
|
content: [
|
|
@@ -6876,7 +7241,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
6876
7241
|
}
|
|
6877
7242
|
try {
|
|
6878
7243
|
const fullPath = path9.join(vaultPath2, note.path);
|
|
6879
|
-
const content = await
|
|
7244
|
+
const content = await fs10.promises.readFile(fullPath, "utf-8");
|
|
6880
7245
|
const lines = content.split("\n");
|
|
6881
7246
|
for (let i = 0; i < lines.length; i++) {
|
|
6882
7247
|
const line = lines[i];
|
|
@@ -6992,7 +7357,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
6992
7357
|
if (include_word_count) {
|
|
6993
7358
|
try {
|
|
6994
7359
|
const fullPath = path9.join(vaultPath2, resolvedPath);
|
|
6995
|
-
const content = await
|
|
7360
|
+
const content = await fs10.promises.readFile(fullPath, "utf-8");
|
|
6996
7361
|
wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
6997
7362
|
} catch {
|
|
6998
7363
|
}
|
|
@@ -7094,7 +7459,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
7094
7459
|
import { z as z6 } from "zod";
|
|
7095
7460
|
|
|
7096
7461
|
// src/tools/read/structure.ts
|
|
7097
|
-
import * as
|
|
7462
|
+
import * as fs11 from "fs";
|
|
7098
7463
|
import * as path10 from "path";
|
|
7099
7464
|
var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
|
|
7100
7465
|
function extractHeadings(content) {
|
|
@@ -7152,7 +7517,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
|
|
|
7152
7517
|
const absolutePath = path10.join(vaultPath2, notePath);
|
|
7153
7518
|
let content;
|
|
7154
7519
|
try {
|
|
7155
|
-
content = await
|
|
7520
|
+
content = await fs11.promises.readFile(absolutePath, "utf-8");
|
|
7156
7521
|
} catch {
|
|
7157
7522
|
return null;
|
|
7158
7523
|
}
|
|
@@ -7175,7 +7540,7 @@ async function getSectionContent(index, notePath, headingText, vaultPath2, inclu
|
|
|
7175
7540
|
const absolutePath = path10.join(vaultPath2, notePath);
|
|
7176
7541
|
let content;
|
|
7177
7542
|
try {
|
|
7178
|
-
content = await
|
|
7543
|
+
content = await fs11.promises.readFile(absolutePath, "utf-8");
|
|
7179
7544
|
} catch {
|
|
7180
7545
|
return null;
|
|
7181
7546
|
}
|
|
@@ -7217,7 +7582,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
7217
7582
|
const absolutePath = path10.join(vaultPath2, note.path);
|
|
7218
7583
|
let content;
|
|
7219
7584
|
try {
|
|
7220
|
-
content = await
|
|
7585
|
+
content = await fs11.promises.readFile(absolutePath, "utf-8");
|
|
7221
7586
|
} catch {
|
|
7222
7587
|
continue;
|
|
7223
7588
|
}
|
|
@@ -7237,7 +7602,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
7237
7602
|
}
|
|
7238
7603
|
|
|
7239
7604
|
// src/tools/read/tasks.ts
|
|
7240
|
-
import * as
|
|
7605
|
+
import * as fs12 from "fs";
|
|
7241
7606
|
import * as path11 from "path";
|
|
7242
7607
|
var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
|
|
7243
7608
|
var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
|
|
@@ -7264,7 +7629,7 @@ function extractDueDate(text) {
|
|
|
7264
7629
|
async function extractTasksFromNote(notePath, absolutePath) {
|
|
7265
7630
|
let content;
|
|
7266
7631
|
try {
|
|
7267
|
-
content = await
|
|
7632
|
+
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
7268
7633
|
} catch {
|
|
7269
7634
|
return [];
|
|
7270
7635
|
}
|
|
@@ -7369,18 +7734,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7369
7734
|
include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
|
|
7370
7735
|
}
|
|
7371
7736
|
},
|
|
7372
|
-
async ({ path:
|
|
7737
|
+
async ({ path: path28, include_content }) => {
|
|
7373
7738
|
const index = getIndex();
|
|
7374
7739
|
const vaultPath2 = getVaultPath();
|
|
7375
|
-
const result = await getNoteStructure(index,
|
|
7740
|
+
const result = await getNoteStructure(index, path28, vaultPath2);
|
|
7376
7741
|
if (!result) {
|
|
7377
7742
|
return {
|
|
7378
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
7743
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path28 }, null, 2) }]
|
|
7379
7744
|
};
|
|
7380
7745
|
}
|
|
7381
7746
|
if (include_content) {
|
|
7382
7747
|
for (const section of result.sections) {
|
|
7383
|
-
const sectionResult = await getSectionContent(index,
|
|
7748
|
+
const sectionResult = await getSectionContent(index, path28, section.heading.text, vaultPath2, true);
|
|
7384
7749
|
if (sectionResult) {
|
|
7385
7750
|
section.content = sectionResult.content;
|
|
7386
7751
|
}
|
|
@@ -7402,15 +7767,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7402
7767
|
include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
|
|
7403
7768
|
}
|
|
7404
7769
|
},
|
|
7405
|
-
async ({ path:
|
|
7770
|
+
async ({ path: path28, heading, include_subheadings }) => {
|
|
7406
7771
|
const index = getIndex();
|
|
7407
7772
|
const vaultPath2 = getVaultPath();
|
|
7408
|
-
const result = await getSectionContent(index,
|
|
7773
|
+
const result = await getSectionContent(index, path28, heading, vaultPath2, include_subheadings);
|
|
7409
7774
|
if (!result) {
|
|
7410
7775
|
return {
|
|
7411
7776
|
content: [{ type: "text", text: JSON.stringify({
|
|
7412
7777
|
error: "Section not found",
|
|
7413
|
-
path:
|
|
7778
|
+
path: path28,
|
|
7414
7779
|
heading
|
|
7415
7780
|
}, null, 2) }]
|
|
7416
7781
|
};
|
|
@@ -7464,16 +7829,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7464
7829
|
offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
7465
7830
|
}
|
|
7466
7831
|
},
|
|
7467
|
-
async ({ path:
|
|
7832
|
+
async ({ path: path28, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
|
|
7468
7833
|
const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
|
|
7469
7834
|
const index = getIndex();
|
|
7470
7835
|
const vaultPath2 = getVaultPath();
|
|
7471
7836
|
const config = getConfig();
|
|
7472
|
-
if (
|
|
7473
|
-
const result2 = await getTasksFromNote(index,
|
|
7837
|
+
if (path28) {
|
|
7838
|
+
const result2 = await getTasksFromNote(index, path28, vaultPath2, config.exclude_task_tags || []);
|
|
7474
7839
|
if (!result2) {
|
|
7475
7840
|
return {
|
|
7476
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
7841
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path28 }, null, 2) }]
|
|
7477
7842
|
};
|
|
7478
7843
|
}
|
|
7479
7844
|
let filtered = result2;
|
|
@@ -7483,7 +7848,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7483
7848
|
const paged2 = filtered.slice(offset, offset + limit);
|
|
7484
7849
|
return {
|
|
7485
7850
|
content: [{ type: "text", text: JSON.stringify({
|
|
7486
|
-
path:
|
|
7851
|
+
path: path28,
|
|
7487
7852
|
total_count: filtered.length,
|
|
7488
7853
|
returned_count: paged2.length,
|
|
7489
7854
|
open: result2.filter((t) => t.status === "open").length,
|
|
@@ -7598,7 +7963,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7598
7963
|
|
|
7599
7964
|
// src/tools/read/migrations.ts
|
|
7600
7965
|
import { z as z7 } from "zod";
|
|
7601
|
-
import * as
|
|
7966
|
+
import * as fs13 from "fs/promises";
|
|
7602
7967
|
import * as path12 from "path";
|
|
7603
7968
|
import matter2 from "gray-matter";
|
|
7604
7969
|
function getNotesInFolder(index, folder) {
|
|
@@ -7614,7 +7979,7 @@ function getNotesInFolder(index, folder) {
|
|
|
7614
7979
|
async function readFileContent(notePath, vaultPath2) {
|
|
7615
7980
|
const fullPath = path12.join(vaultPath2, notePath);
|
|
7616
7981
|
try {
|
|
7617
|
-
return await
|
|
7982
|
+
return await fs13.readFile(fullPath, "utf-8");
|
|
7618
7983
|
} catch {
|
|
7619
7984
|
return null;
|
|
7620
7985
|
}
|
|
@@ -7622,7 +7987,7 @@ async function readFileContent(notePath, vaultPath2) {
|
|
|
7622
7987
|
async function writeFileContent(notePath, vaultPath2, content) {
|
|
7623
7988
|
const fullPath = path12.join(vaultPath2, notePath);
|
|
7624
7989
|
try {
|
|
7625
|
-
await
|
|
7990
|
+
await fs13.writeFile(fullPath, content, "utf-8");
|
|
7626
7991
|
return true;
|
|
7627
7992
|
} catch {
|
|
7628
7993
|
return false;
|
|
@@ -7800,303 +8165,12 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
|
|
|
7800
8165
|
}
|
|
7801
8166
|
|
|
7802
8167
|
// src/tools/read/graphAnalysis.ts
|
|
8168
|
+
import fs14 from "node:fs";
|
|
8169
|
+
import path13 from "node:path";
|
|
7803
8170
|
import { z as z8 } from "zod";
|
|
7804
|
-
function registerGraphAnalysisTools(server2, getIndex, getVaultPath) {
|
|
7805
|
-
server2.registerTool(
|
|
7806
|
-
"graph_analysis",
|
|
7807
|
-
{
|
|
7808
|
-
title: "Graph Analysis",
|
|
7809
|
-
description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (disconnected content)\n- "dead_ends": Notes with backlinks but no outgoing links\n- "sources": Notes with outgoing links but no backlinks\n- "hubs": Highly connected notes (many links to/from)\n- "stale": Important notes (by backlink count) not recently modified\n\nExample: graph_analysis({ analysis: "hubs", limit: 10 })\nExample: graph_analysis({ analysis: "stale", days: 30, min_backlinks: 3 })',
|
|
7810
|
-
inputSchema: {
|
|
7811
|
-
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale"]).describe("Type of graph analysis to perform"),
|
|
7812
|
-
folder: z8.string().optional().describe("Limit to notes in this folder (orphans, dead_ends, sources)"),
|
|
7813
|
-
min_links: z8.coerce.number().default(5).describe("Minimum total connections for hubs"),
|
|
7814
|
-
min_backlinks: z8.coerce.number().default(1).describe("Minimum backlinks (dead_ends, stale)"),
|
|
7815
|
-
min_outlinks: z8.coerce.number().default(1).describe("Minimum outlinks (sources)"),
|
|
7816
|
-
days: z8.coerce.number().optional().describe("Notes not modified in this many days (stale, required)"),
|
|
7817
|
-
limit: z8.coerce.number().default(50).describe("Maximum number of results to return"),
|
|
7818
|
-
offset: z8.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
7819
|
-
}
|
|
7820
|
-
},
|
|
7821
|
-
async ({ analysis, folder, min_links, min_backlinks, min_outlinks, days, limit: requestedLimit, offset }) => {
|
|
7822
|
-
requireIndex();
|
|
7823
|
-
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
7824
|
-
const index = getIndex();
|
|
7825
|
-
switch (analysis) {
|
|
7826
|
-
case "orphans": {
|
|
7827
|
-
const allOrphans = findOrphanNotes(index, folder);
|
|
7828
|
-
const orphans = allOrphans.slice(offset, offset + limit);
|
|
7829
|
-
return {
|
|
7830
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7831
|
-
analysis: "orphans",
|
|
7832
|
-
orphan_count: allOrphans.length,
|
|
7833
|
-
returned_count: orphans.length,
|
|
7834
|
-
folder,
|
|
7835
|
-
orphans: orphans.map((o) => ({
|
|
7836
|
-
path: o.path,
|
|
7837
|
-
title: o.title,
|
|
7838
|
-
modified: o.modified.toISOString()
|
|
7839
|
-
}))
|
|
7840
|
-
}, null, 2) }]
|
|
7841
|
-
};
|
|
7842
|
-
}
|
|
7843
|
-
case "dead_ends": {
|
|
7844
|
-
const allResults = findDeadEnds(index, folder, min_backlinks);
|
|
7845
|
-
const result = allResults.slice(offset, offset + limit);
|
|
7846
|
-
return {
|
|
7847
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7848
|
-
analysis: "dead_ends",
|
|
7849
|
-
criteria: { folder, min_backlinks },
|
|
7850
|
-
total_count: allResults.length,
|
|
7851
|
-
returned_count: result.length,
|
|
7852
|
-
dead_ends: result
|
|
7853
|
-
}, null, 2) }]
|
|
7854
|
-
};
|
|
7855
|
-
}
|
|
7856
|
-
case "sources": {
|
|
7857
|
-
const allResults = findSources(index, folder, min_outlinks);
|
|
7858
|
-
const result = allResults.slice(offset, offset + limit);
|
|
7859
|
-
return {
|
|
7860
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7861
|
-
analysis: "sources",
|
|
7862
|
-
criteria: { folder, min_outlinks },
|
|
7863
|
-
total_count: allResults.length,
|
|
7864
|
-
returned_count: result.length,
|
|
7865
|
-
sources: result
|
|
7866
|
-
}, null, 2) }]
|
|
7867
|
-
};
|
|
7868
|
-
}
|
|
7869
|
-
case "hubs": {
|
|
7870
|
-
const allHubs = findHubNotes(index, min_links);
|
|
7871
|
-
const hubs = allHubs.slice(offset, offset + limit);
|
|
7872
|
-
return {
|
|
7873
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7874
|
-
analysis: "hubs",
|
|
7875
|
-
hub_count: allHubs.length,
|
|
7876
|
-
returned_count: hubs.length,
|
|
7877
|
-
min_links,
|
|
7878
|
-
hubs: hubs.map((h) => ({
|
|
7879
|
-
path: h.path,
|
|
7880
|
-
title: h.title,
|
|
7881
|
-
backlink_count: h.backlink_count,
|
|
7882
|
-
forward_link_count: h.forward_link_count,
|
|
7883
|
-
total_connections: h.total_connections
|
|
7884
|
-
}))
|
|
7885
|
-
}, null, 2) }]
|
|
7886
|
-
};
|
|
7887
|
-
}
|
|
7888
|
-
case "stale": {
|
|
7889
|
-
if (days === void 0) {
|
|
7890
|
-
return {
|
|
7891
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7892
|
-
error: "days parameter is required for stale analysis"
|
|
7893
|
-
}, null, 2) }]
|
|
7894
|
-
};
|
|
7895
|
-
}
|
|
7896
|
-
const result = getStaleNotes(index, days, min_backlinks).slice(0, limit);
|
|
7897
|
-
return {
|
|
7898
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7899
|
-
analysis: "stale",
|
|
7900
|
-
criteria: { days, min_backlinks },
|
|
7901
|
-
count: result.length,
|
|
7902
|
-
notes: result.map((n) => ({
|
|
7903
|
-
...n,
|
|
7904
|
-
modified: n.modified.toISOString()
|
|
7905
|
-
}))
|
|
7906
|
-
}, null, 2) }]
|
|
7907
|
-
};
|
|
7908
|
-
}
|
|
7909
|
-
}
|
|
7910
|
-
}
|
|
7911
|
-
);
|
|
7912
|
-
}
|
|
7913
|
-
|
|
7914
|
-
// src/tools/read/vaultSchema.ts
|
|
7915
|
-
import { z as z9 } from "zod";
|
|
7916
8171
|
|
|
7917
|
-
// src/tools/read/
|
|
8172
|
+
// src/tools/read/schema.ts
|
|
7918
8173
|
function getValueType(value) {
|
|
7919
|
-
if (value === null) return "null";
|
|
7920
|
-
if (value === void 0) return "undefined";
|
|
7921
|
-
if (Array.isArray(value)) return "array";
|
|
7922
|
-
if (value instanceof Date) return "date";
|
|
7923
|
-
return typeof value;
|
|
7924
|
-
}
|
|
7925
|
-
function getFrontmatterSchema(index) {
|
|
7926
|
-
const fieldMap = /* @__PURE__ */ new Map();
|
|
7927
|
-
let notesWithFrontmatter = 0;
|
|
7928
|
-
for (const note of index.notes.values()) {
|
|
7929
|
-
const fm = note.frontmatter;
|
|
7930
|
-
if (!fm || Object.keys(fm).length === 0) continue;
|
|
7931
|
-
notesWithFrontmatter++;
|
|
7932
|
-
for (const [key, value] of Object.entries(fm)) {
|
|
7933
|
-
if (!fieldMap.has(key)) {
|
|
7934
|
-
fieldMap.set(key, {
|
|
7935
|
-
types: /* @__PURE__ */ new Set(),
|
|
7936
|
-
count: 0,
|
|
7937
|
-
examples: [],
|
|
7938
|
-
notes: []
|
|
7939
|
-
});
|
|
7940
|
-
}
|
|
7941
|
-
const info = fieldMap.get(key);
|
|
7942
|
-
info.count++;
|
|
7943
|
-
info.types.add(getValueType(value));
|
|
7944
|
-
if (info.examples.length < 5) {
|
|
7945
|
-
const valueStr = JSON.stringify(value);
|
|
7946
|
-
const existingStrs = info.examples.map((e) => JSON.stringify(e));
|
|
7947
|
-
if (!existingStrs.includes(valueStr)) {
|
|
7948
|
-
info.examples.push(value);
|
|
7949
|
-
}
|
|
7950
|
-
}
|
|
7951
|
-
if (info.notes.length < 5) {
|
|
7952
|
-
info.notes.push(note.path);
|
|
7953
|
-
}
|
|
7954
|
-
}
|
|
7955
|
-
}
|
|
7956
|
-
const fields = Array.from(fieldMap.entries()).map(([name, info]) => ({
|
|
7957
|
-
name,
|
|
7958
|
-
types: Array.from(info.types),
|
|
7959
|
-
count: info.count,
|
|
7960
|
-
examples: info.examples,
|
|
7961
|
-
notes_sample: info.notes
|
|
7962
|
-
})).sort((a, b) => b.count - a.count);
|
|
7963
|
-
return {
|
|
7964
|
-
total_notes: index.notes.size,
|
|
7965
|
-
notes_with_frontmatter: notesWithFrontmatter,
|
|
7966
|
-
field_count: fields.length,
|
|
7967
|
-
fields
|
|
7968
|
-
};
|
|
7969
|
-
}
|
|
7970
|
-
function getFieldValues(index, fieldName) {
|
|
7971
|
-
const valueMap = /* @__PURE__ */ new Map();
|
|
7972
|
-
let totalWithField = 0;
|
|
7973
|
-
for (const note of index.notes.values()) {
|
|
7974
|
-
const value = note.frontmatter[fieldName];
|
|
7975
|
-
if (value === void 0) continue;
|
|
7976
|
-
totalWithField++;
|
|
7977
|
-
const values = Array.isArray(value) ? value : [value];
|
|
7978
|
-
for (const v of values) {
|
|
7979
|
-
const key = JSON.stringify(v);
|
|
7980
|
-
if (!valueMap.has(key)) {
|
|
7981
|
-
valueMap.set(key, {
|
|
7982
|
-
value: v,
|
|
7983
|
-
count: 0,
|
|
7984
|
-
notes: []
|
|
7985
|
-
});
|
|
7986
|
-
}
|
|
7987
|
-
const info = valueMap.get(key);
|
|
7988
|
-
info.count++;
|
|
7989
|
-
info.notes.push(note.path);
|
|
7990
|
-
}
|
|
7991
|
-
}
|
|
7992
|
-
const valuesList = Array.from(valueMap.values()).sort((a, b) => b.count - a.count);
|
|
7993
|
-
return {
|
|
7994
|
-
field: fieldName,
|
|
7995
|
-
total_notes_with_field: totalWithField,
|
|
7996
|
-
unique_values: valuesList.length,
|
|
7997
|
-
values: valuesList
|
|
7998
|
-
};
|
|
7999
|
-
}
|
|
8000
|
-
function findFrontmatterInconsistencies(index) {
|
|
8001
|
-
const schema = getFrontmatterSchema(index);
|
|
8002
|
-
const inconsistencies = [];
|
|
8003
|
-
for (const field of schema.fields) {
|
|
8004
|
-
if (field.types.length > 1) {
|
|
8005
|
-
const examples = [];
|
|
8006
|
-
for (const note of index.notes.values()) {
|
|
8007
|
-
const value = note.frontmatter[field.name];
|
|
8008
|
-
if (value === void 0) continue;
|
|
8009
|
-
const type = getValueType(value);
|
|
8010
|
-
if (!examples.some((e) => e.type === type)) {
|
|
8011
|
-
examples.push({
|
|
8012
|
-
type,
|
|
8013
|
-
value,
|
|
8014
|
-
note: note.path
|
|
8015
|
-
});
|
|
8016
|
-
}
|
|
8017
|
-
if (examples.length >= field.types.length) break;
|
|
8018
|
-
}
|
|
8019
|
-
inconsistencies.push({
|
|
8020
|
-
field: field.name,
|
|
8021
|
-
types_found: field.types,
|
|
8022
|
-
examples
|
|
8023
|
-
});
|
|
8024
|
-
}
|
|
8025
|
-
}
|
|
8026
|
-
return inconsistencies;
|
|
8027
|
-
}
|
|
8028
|
-
function validateFrontmatter(index, schema, folder) {
|
|
8029
|
-
const results = [];
|
|
8030
|
-
for (const note of index.notes.values()) {
|
|
8031
|
-
if (folder && !note.path.startsWith(folder)) continue;
|
|
8032
|
-
const issues = [];
|
|
8033
|
-
for (const [fieldName, fieldSchema] of Object.entries(schema)) {
|
|
8034
|
-
const value = note.frontmatter[fieldName];
|
|
8035
|
-
if (fieldSchema.required && value === void 0) {
|
|
8036
|
-
issues.push({
|
|
8037
|
-
field: fieldName,
|
|
8038
|
-
issue: "missing",
|
|
8039
|
-
expected: "value required"
|
|
8040
|
-
});
|
|
8041
|
-
continue;
|
|
8042
|
-
}
|
|
8043
|
-
if (value === void 0) continue;
|
|
8044
|
-
if (fieldSchema.type) {
|
|
8045
|
-
const actualType = getValueType(value);
|
|
8046
|
-
const allowedTypes = Array.isArray(fieldSchema.type) ? fieldSchema.type : [fieldSchema.type];
|
|
8047
|
-
if (!allowedTypes.includes(actualType)) {
|
|
8048
|
-
issues.push({
|
|
8049
|
-
field: fieldName,
|
|
8050
|
-
issue: "wrong_type",
|
|
8051
|
-
expected: allowedTypes.join(" | "),
|
|
8052
|
-
actual: actualType
|
|
8053
|
-
});
|
|
8054
|
-
}
|
|
8055
|
-
}
|
|
8056
|
-
if (fieldSchema.values) {
|
|
8057
|
-
const valueStr = JSON.stringify(value);
|
|
8058
|
-
const allowedStrs = fieldSchema.values.map((v) => JSON.stringify(v));
|
|
8059
|
-
if (!allowedStrs.includes(valueStr)) {
|
|
8060
|
-
issues.push({
|
|
8061
|
-
field: fieldName,
|
|
8062
|
-
issue: "invalid_value",
|
|
8063
|
-
expected: fieldSchema.values.map((v) => String(v)).join(" | "),
|
|
8064
|
-
actual: String(value)
|
|
8065
|
-
});
|
|
8066
|
-
}
|
|
8067
|
-
}
|
|
8068
|
-
}
|
|
8069
|
-
if (issues.length > 0) {
|
|
8070
|
-
results.push({
|
|
8071
|
-
path: note.path,
|
|
8072
|
-
issues
|
|
8073
|
-
});
|
|
8074
|
-
}
|
|
8075
|
-
}
|
|
8076
|
-
return results;
|
|
8077
|
-
}
|
|
8078
|
-
function findMissingFrontmatter(index, folderSchemas) {
|
|
8079
|
-
const results = [];
|
|
8080
|
-
for (const note of index.notes.values()) {
|
|
8081
|
-
for (const [folder, requiredFields] of Object.entries(folderSchemas)) {
|
|
8082
|
-
if (!note.path.startsWith(folder)) continue;
|
|
8083
|
-
const missing = requiredFields.filter(
|
|
8084
|
-
(field) => note.frontmatter[field] === void 0
|
|
8085
|
-
);
|
|
8086
|
-
if (missing.length > 0) {
|
|
8087
|
-
results.push({
|
|
8088
|
-
path: note.path,
|
|
8089
|
-
folder,
|
|
8090
|
-
missing_fields: missing
|
|
8091
|
-
});
|
|
8092
|
-
}
|
|
8093
|
-
}
|
|
8094
|
-
}
|
|
8095
|
-
return results;
|
|
8096
|
-
}
|
|
8097
|
-
|
|
8098
|
-
// src/tools/read/schema.ts
|
|
8099
|
-
function getValueType2(value) {
|
|
8100
8174
|
if (value === null) return "null";
|
|
8101
8175
|
if (value === void 0) return "undefined";
|
|
8102
8176
|
if (Array.isArray(value)) {
|
|
@@ -8181,7 +8255,7 @@ function inferFolderConventions(index, folder, minConfidence = 0.5) {
|
|
|
8181
8255
|
}
|
|
8182
8256
|
const stats = fieldStats.get(key);
|
|
8183
8257
|
stats.count++;
|
|
8184
|
-
const type =
|
|
8258
|
+
const type = getValueType(value);
|
|
8185
8259
|
stats.types.set(type, (stats.types.get(type) || 0) + 1);
|
|
8186
8260
|
const valueStr = JSON.stringify(value);
|
|
8187
8261
|
stats.values.set(valueStr, (stats.values.get(valueStr) || 0) + 1);
|
|
@@ -8313,7 +8387,7 @@ function suggestFieldValues(index, field, context) {
|
|
|
8313
8387
|
const value = note.frontmatter[field];
|
|
8314
8388
|
if (value === void 0) continue;
|
|
8315
8389
|
totalWithField++;
|
|
8316
|
-
const type =
|
|
8390
|
+
const type = getValueType(value);
|
|
8317
8391
|
typeCounts.set(type, (typeCounts.get(type) || 0) + 1);
|
|
8318
8392
|
const values = Array.isArray(value) ? value : [value];
|
|
8319
8393
|
for (const v of values) {
|
|
@@ -8372,148 +8446,480 @@ function suggestFieldValues(index, field, context) {
|
|
|
8372
8446
|
is_enumerable: isEnumerable
|
|
8373
8447
|
};
|
|
8374
8448
|
}
|
|
8449
|
+
var SKIP_CONTRADICTION_FIELDS = /* @__PURE__ */ new Set([
|
|
8450
|
+
"title",
|
|
8451
|
+
"created",
|
|
8452
|
+
"modified",
|
|
8453
|
+
"path",
|
|
8454
|
+
"aliases",
|
|
8455
|
+
"tags",
|
|
8456
|
+
"date",
|
|
8457
|
+
"updated",
|
|
8458
|
+
"word_count",
|
|
8459
|
+
"link_count"
|
|
8460
|
+
]);
|
|
8461
|
+
function findContradictions(index, entity) {
|
|
8462
|
+
const contradictions = [];
|
|
8463
|
+
const entitiesToCheck = [];
|
|
8464
|
+
if (entity) {
|
|
8465
|
+
const normalized = entity.toLowerCase();
|
|
8466
|
+
const entityPath = index.entities.get(normalized);
|
|
8467
|
+
if (entityPath) {
|
|
8468
|
+
entitiesToCheck.push([normalized, entityPath]);
|
|
8469
|
+
}
|
|
8470
|
+
} else {
|
|
8471
|
+
for (const [name, entityPath] of index.entities) {
|
|
8472
|
+
entitiesToCheck.push([name, entityPath]);
|
|
8473
|
+
}
|
|
8474
|
+
}
|
|
8475
|
+
for (const [entityName, _entityPath] of entitiesToCheck) {
|
|
8476
|
+
const backlinks = index.backlinks.get(entityName);
|
|
8477
|
+
if (!backlinks || backlinks.length < 2) continue;
|
|
8478
|
+
const sourcePaths = [...new Set(backlinks.map((bl) => bl.source))];
|
|
8479
|
+
if (sourcePaths.length < 2) continue;
|
|
8480
|
+
const notesFrontmatter = [];
|
|
8481
|
+
for (const srcPath of sourcePaths) {
|
|
8482
|
+
const note = index.notes.get(srcPath);
|
|
8483
|
+
if (note && Object.keys(note.frontmatter).length > 0) {
|
|
8484
|
+
notesFrontmatter.push({ path: srcPath, fm: note.frontmatter });
|
|
8485
|
+
}
|
|
8486
|
+
}
|
|
8487
|
+
if (notesFrontmatter.length < 2) continue;
|
|
8488
|
+
const allFields = /* @__PURE__ */ new Set();
|
|
8489
|
+
for (const { fm } of notesFrontmatter) {
|
|
8490
|
+
for (const key of Object.keys(fm)) {
|
|
8491
|
+
if (!SKIP_CONTRADICTION_FIELDS.has(key)) {
|
|
8492
|
+
allFields.add(key);
|
|
8493
|
+
}
|
|
8494
|
+
}
|
|
8495
|
+
}
|
|
8496
|
+
for (const field of allFields) {
|
|
8497
|
+
const valueMap = /* @__PURE__ */ new Map();
|
|
8498
|
+
for (const { path: notePath, fm } of notesFrontmatter) {
|
|
8499
|
+
if (fm[field] === void 0) continue;
|
|
8500
|
+
const key = JSON.stringify(fm[field]);
|
|
8501
|
+
if (!valueMap.has(key)) {
|
|
8502
|
+
valueMap.set(key, []);
|
|
8503
|
+
}
|
|
8504
|
+
valueMap.get(key).push(notePath);
|
|
8505
|
+
}
|
|
8506
|
+
if (valueMap.size >= 2) {
|
|
8507
|
+
contradictions.push({
|
|
8508
|
+
entity: entityName,
|
|
8509
|
+
field,
|
|
8510
|
+
values: Array.from(valueMap.entries()).map(([serialized, notes]) => ({
|
|
8511
|
+
value: JSON.parse(serialized),
|
|
8512
|
+
notes
|
|
8513
|
+
}))
|
|
8514
|
+
});
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
return contradictions;
|
|
8519
|
+
}
|
|
8375
8520
|
|
|
8376
|
-
// src/
|
|
8377
|
-
function
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
{
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
})).optional().describe("Schema to validate against (validate mode)"),
|
|
8401
|
-
folder_schemas: z9.record(z9.array(z9.string())).optional().describe("Map of folder paths to required fields (missing mode)"),
|
|
8402
|
-
min_confidence: z9.coerce.number().min(0).max(1).optional().describe("Minimum confidence threshold (conventions)"),
|
|
8403
|
-
min_frequency: z9.coerce.number().min(0).max(1).optional().describe("Minimum field frequency (incomplete)"),
|
|
8404
|
-
existing_frontmatter: z9.record(z9.unknown()).optional().describe("Existing frontmatter for context (suggest_values)"),
|
|
8405
|
-
limit: z9.coerce.number().default(50).describe("Maximum results to return"),
|
|
8406
|
-
offset: z9.coerce.number().default(0).describe("Number of results to skip")
|
|
8521
|
+
// src/core/shared/graphSnapshots.ts
|
|
8522
|
+
function computeGraphMetrics(index) {
|
|
8523
|
+
const noteCount = index.notes.size;
|
|
8524
|
+
if (noteCount === 0) {
|
|
8525
|
+
return {
|
|
8526
|
+
avg_degree: 0,
|
|
8527
|
+
max_degree: 0,
|
|
8528
|
+
cluster_count: 0,
|
|
8529
|
+
largest_cluster_size: 0,
|
|
8530
|
+
hub_scores_top10: []
|
|
8531
|
+
};
|
|
8532
|
+
}
|
|
8533
|
+
const degreeMap = /* @__PURE__ */ new Map();
|
|
8534
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
8535
|
+
for (const [notePath, note] of index.notes) {
|
|
8536
|
+
if (!adjacency.has(notePath)) adjacency.set(notePath, /* @__PURE__ */ new Set());
|
|
8537
|
+
let degree = note.outlinks.length;
|
|
8538
|
+
for (const link of note.outlinks) {
|
|
8539
|
+
const targetLower = link.target.toLowerCase();
|
|
8540
|
+
const resolvedPath = index.entities.get(targetLower);
|
|
8541
|
+
if (resolvedPath && index.notes.has(resolvedPath)) {
|
|
8542
|
+
adjacency.get(notePath).add(resolvedPath);
|
|
8543
|
+
if (!adjacency.has(resolvedPath)) adjacency.set(resolvedPath, /* @__PURE__ */ new Set());
|
|
8544
|
+
adjacency.get(resolvedPath).add(notePath);
|
|
8407
8545
|
}
|
|
8408
|
-
}
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8546
|
+
}
|
|
8547
|
+
degreeMap.set(notePath, degree);
|
|
8548
|
+
}
|
|
8549
|
+
for (const [target, backlinks] of index.backlinks) {
|
|
8550
|
+
const targetLower = target.toLowerCase();
|
|
8551
|
+
const resolvedPath = index.entities.get(targetLower);
|
|
8552
|
+
if (resolvedPath && degreeMap.has(resolvedPath)) {
|
|
8553
|
+
degreeMap.set(resolvedPath, degreeMap.get(resolvedPath) + backlinks.length);
|
|
8554
|
+
}
|
|
8555
|
+
}
|
|
8556
|
+
let totalDegree = 0;
|
|
8557
|
+
let maxDegree = 0;
|
|
8558
|
+
let maxDegreeNote = "";
|
|
8559
|
+
for (const [notePath, degree] of degreeMap) {
|
|
8560
|
+
totalDegree += degree;
|
|
8561
|
+
if (degree > maxDegree) {
|
|
8562
|
+
maxDegree = degree;
|
|
8563
|
+
maxDegreeNote = notePath;
|
|
8564
|
+
}
|
|
8565
|
+
}
|
|
8566
|
+
const avgDegree = noteCount > 0 ? Math.round(totalDegree / noteCount * 100) / 100 : 0;
|
|
8567
|
+
const visited = /* @__PURE__ */ new Set();
|
|
8568
|
+
const clusters = [];
|
|
8569
|
+
for (const notePath of index.notes.keys()) {
|
|
8570
|
+
if (visited.has(notePath)) continue;
|
|
8571
|
+
const queue = [notePath];
|
|
8572
|
+
visited.add(notePath);
|
|
8573
|
+
let clusterSize = 0;
|
|
8574
|
+
while (queue.length > 0) {
|
|
8575
|
+
const current = queue.shift();
|
|
8576
|
+
clusterSize++;
|
|
8577
|
+
const neighbors = adjacency.get(current);
|
|
8578
|
+
if (neighbors) {
|
|
8579
|
+
for (const neighbor of neighbors) {
|
|
8580
|
+
if (!visited.has(neighbor)) {
|
|
8581
|
+
visited.add(neighbor);
|
|
8582
|
+
queue.push(neighbor);
|
|
8427
8583
|
}
|
|
8428
|
-
const result = getFieldValues(index, params.field);
|
|
8429
|
-
return {
|
|
8430
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
8431
|
-
};
|
|
8432
8584
|
}
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8585
|
+
}
|
|
8586
|
+
}
|
|
8587
|
+
clusters.push(clusterSize);
|
|
8588
|
+
}
|
|
8589
|
+
const clusterCount = clusters.length;
|
|
8590
|
+
const largestClusterSize = clusters.length > 0 ? Math.max(...clusters) : 0;
|
|
8591
|
+
const sorted = Array.from(degreeMap.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
8592
|
+
const hubScoresTop10 = sorted.map(([notePath, degree]) => {
|
|
8593
|
+
const note = index.notes.get(notePath);
|
|
8594
|
+
return {
|
|
8595
|
+
entity: note?.title ?? notePath,
|
|
8596
|
+
degree
|
|
8597
|
+
};
|
|
8598
|
+
});
|
|
8599
|
+
return {
|
|
8600
|
+
avg_degree: avgDegree,
|
|
8601
|
+
max_degree: maxDegree,
|
|
8602
|
+
cluster_count: clusterCount,
|
|
8603
|
+
largest_cluster_size: largestClusterSize,
|
|
8604
|
+
hub_scores_top10: hubScoresTop10
|
|
8605
|
+
};
|
|
8606
|
+
}
|
|
8607
|
+
function recordGraphSnapshot(stateDb2, metrics) {
|
|
8608
|
+
const timestamp = Date.now();
|
|
8609
|
+
const insert = stateDb2.db.prepare(
|
|
8610
|
+
"INSERT INTO graph_snapshots (timestamp, metric, value, details) VALUES (?, ?, ?, ?)"
|
|
8611
|
+
);
|
|
8612
|
+
const transaction = stateDb2.db.transaction(() => {
|
|
8613
|
+
insert.run(timestamp, "avg_degree", metrics.avg_degree, null);
|
|
8614
|
+
insert.run(timestamp, "max_degree", metrics.max_degree, null);
|
|
8615
|
+
insert.run(timestamp, "cluster_count", metrics.cluster_count, null);
|
|
8616
|
+
insert.run(timestamp, "largest_cluster_size", metrics.largest_cluster_size, null);
|
|
8617
|
+
insert.run(
|
|
8618
|
+
timestamp,
|
|
8619
|
+
"hub_scores_top10",
|
|
8620
|
+
metrics.hub_scores_top10.length,
|
|
8621
|
+
JSON.stringify(metrics.hub_scores_top10)
|
|
8622
|
+
);
|
|
8623
|
+
});
|
|
8624
|
+
transaction();
|
|
8625
|
+
}
|
|
8626
|
+
function getGraphEvolution(stateDb2, daysBack = 30) {
|
|
8627
|
+
const now = Date.now();
|
|
8628
|
+
const cutoff = now - daysBack * 24 * 60 * 60 * 1e3;
|
|
8629
|
+
const SCALAR_METRICS = ["avg_degree", "max_degree", "cluster_count", "largest_cluster_size"];
|
|
8630
|
+
const latestRows = stateDb2.db.prepare(`
|
|
8631
|
+
SELECT metric, value FROM graph_snapshots
|
|
8632
|
+
WHERE metric IN ('avg_degree', 'max_degree', 'cluster_count', 'largest_cluster_size')
|
|
8633
|
+
GROUP BY metric
|
|
8634
|
+
HAVING timestamp = MAX(timestamp)
|
|
8635
|
+
`).all();
|
|
8636
|
+
const currentValues = /* @__PURE__ */ new Map();
|
|
8637
|
+
for (const row of latestRows) {
|
|
8638
|
+
currentValues.set(row.metric, row.value);
|
|
8639
|
+
}
|
|
8640
|
+
const previousRows = stateDb2.db.prepare(`
|
|
8641
|
+
SELECT metric, value FROM graph_snapshots
|
|
8642
|
+
WHERE metric IN ('avg_degree', 'max_degree', 'cluster_count', 'largest_cluster_size')
|
|
8643
|
+
AND timestamp >= ? AND timestamp <= ?
|
|
8644
|
+
GROUP BY metric
|
|
8645
|
+
HAVING timestamp = MIN(timestamp)
|
|
8646
|
+
`).all(cutoff, cutoff + 24 * 60 * 60 * 1e3);
|
|
8647
|
+
const previousValues = /* @__PURE__ */ new Map();
|
|
8648
|
+
for (const row of previousRows) {
|
|
8649
|
+
previousValues.set(row.metric, row.value);
|
|
8650
|
+
}
|
|
8651
|
+
if (previousValues.size === 0) {
|
|
8652
|
+
const fallbackRows = stateDb2.db.prepare(`
|
|
8653
|
+
SELECT metric, value FROM graph_snapshots
|
|
8654
|
+
WHERE metric IN ('avg_degree', 'max_degree', 'cluster_count', 'largest_cluster_size')
|
|
8655
|
+
AND timestamp >= ?
|
|
8656
|
+
GROUP BY metric
|
|
8657
|
+
HAVING timestamp = MIN(timestamp)
|
|
8658
|
+
`).all(cutoff);
|
|
8659
|
+
for (const row of fallbackRows) {
|
|
8660
|
+
previousValues.set(row.metric, row.value);
|
|
8661
|
+
}
|
|
8662
|
+
}
|
|
8663
|
+
const evolutions = [];
|
|
8664
|
+
for (const metric of SCALAR_METRICS) {
|
|
8665
|
+
const current = currentValues.get(metric) ?? 0;
|
|
8666
|
+
const previous = previousValues.get(metric) ?? current;
|
|
8667
|
+
const delta = current - previous;
|
|
8668
|
+
const deltaPct = previous !== 0 ? Math.round(delta / previous * 1e4) / 100 : delta !== 0 ? 100 : 0;
|
|
8669
|
+
let direction = "stable";
|
|
8670
|
+
if (delta > 0) direction = "up";
|
|
8671
|
+
if (delta < 0) direction = "down";
|
|
8672
|
+
evolutions.push({
|
|
8673
|
+
metric,
|
|
8674
|
+
current,
|
|
8675
|
+
previous,
|
|
8676
|
+
delta,
|
|
8677
|
+
delta_percent: deltaPct,
|
|
8678
|
+
direction
|
|
8679
|
+
});
|
|
8680
|
+
}
|
|
8681
|
+
return evolutions;
|
|
8682
|
+
}
|
|
8683
|
+
function getEmergingHubs(stateDb2, daysBack = 30) {
|
|
8684
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
8685
|
+
const latestRow = stateDb2.db.prepare(
|
|
8686
|
+
`SELECT details FROM graph_snapshots
|
|
8687
|
+
WHERE metric = 'hub_scores_top10'
|
|
8688
|
+
ORDER BY timestamp DESC LIMIT 1`
|
|
8689
|
+
).get();
|
|
8690
|
+
const previousRow = stateDb2.db.prepare(
|
|
8691
|
+
`SELECT details FROM graph_snapshots
|
|
8692
|
+
WHERE metric = 'hub_scores_top10' AND timestamp >= ?
|
|
8693
|
+
ORDER BY timestamp ASC LIMIT 1`
|
|
8694
|
+
).get(cutoff);
|
|
8695
|
+
if (!latestRow?.details) return [];
|
|
8696
|
+
const currentHubs = JSON.parse(latestRow.details);
|
|
8697
|
+
const previousHubs = previousRow?.details ? JSON.parse(previousRow.details) : [];
|
|
8698
|
+
const previousMap = /* @__PURE__ */ new Map();
|
|
8699
|
+
for (const hub of previousHubs) {
|
|
8700
|
+
previousMap.set(hub.entity, hub.degree);
|
|
8701
|
+
}
|
|
8702
|
+
const emerging = currentHubs.map((hub) => {
|
|
8703
|
+
const prevDegree = previousMap.get(hub.entity) ?? 0;
|
|
8704
|
+
return {
|
|
8705
|
+
entity: hub.entity,
|
|
8706
|
+
current_degree: hub.degree,
|
|
8707
|
+
previous_degree: prevDegree,
|
|
8708
|
+
growth: hub.degree - prevDegree
|
|
8709
|
+
};
|
|
8710
|
+
});
|
|
8711
|
+
emerging.sort((a, b) => b.growth - a.growth);
|
|
8712
|
+
return emerging;
|
|
8713
|
+
}
|
|
8714
|
+
function purgeOldSnapshots(stateDb2, retentionDays = 90) {
|
|
8715
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
8716
|
+
const result = stateDb2.db.prepare(
|
|
8717
|
+
"DELETE FROM graph_snapshots WHERE timestamp < ?"
|
|
8718
|
+
).run(cutoff);
|
|
8719
|
+
return result.changes;
|
|
8720
|
+
}
|
|
8721
|
+
|
|
8722
|
+
// src/tools/read/graphAnalysis.ts
|
|
8723
|
+
function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
8724
|
+
server2.registerTool(
|
|
8725
|
+
"graph_analysis",
|
|
8726
|
+
{
|
|
8727
|
+
title: "Graph Analysis",
|
|
8728
|
+
description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (disconnected content)\n- "dead_ends": Notes with backlinks but no outgoing links\n- "sources": Notes with outgoing links but no backlinks\n- "hubs": Highly connected notes (many links to/from)\n- "stale": Important notes (by backlink count) not recently modified\n- "immature": Notes scored by maturity (word count, links, frontmatter completeness, backlinks)\n- "evolution": Graph topology metrics over time (avg_degree, cluster_count, etc.)\n- "emerging_hubs": Entities growing fastest in connection count\n\nExample: graph_analysis({ analysis: "hubs", limit: 10 })\nExample: graph_analysis({ analysis: "stale", days: 30, min_backlinks: 3 })\nExample: graph_analysis({ analysis: "immature", folder: "projects", limit: 20 })\nExample: graph_analysis({ analysis: "evolution", days: 30 })\nExample: graph_analysis({ analysis: "emerging_hubs", days: 30 })',
|
|
8729
|
+
inputSchema: {
|
|
8730
|
+
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale", "immature", "evolution", "emerging_hubs"]).describe("Type of graph analysis to perform"),
|
|
8731
|
+
folder: z8.string().optional().describe("Limit to notes in this folder (orphans, dead_ends, sources)"),
|
|
8732
|
+
min_links: z8.coerce.number().default(5).describe("Minimum total connections for hubs"),
|
|
8733
|
+
min_backlinks: z8.coerce.number().default(1).describe("Minimum backlinks (dead_ends, stale)"),
|
|
8734
|
+
min_outlinks: z8.coerce.number().default(1).describe("Minimum outlinks (sources)"),
|
|
8735
|
+
days: z8.coerce.number().optional().describe("Notes not modified in this many days (stale, required)"),
|
|
8736
|
+
limit: z8.coerce.number().default(50).describe("Maximum number of results to return"),
|
|
8737
|
+
offset: z8.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
8738
|
+
}
|
|
8739
|
+
},
|
|
8740
|
+
async ({ analysis, folder, min_links, min_backlinks, min_outlinks, days, limit: requestedLimit, offset }) => {
|
|
8741
|
+
requireIndex();
|
|
8742
|
+
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
8743
|
+
const index = getIndex();
|
|
8744
|
+
switch (analysis) {
|
|
8745
|
+
case "orphans": {
|
|
8746
|
+
const allOrphans = findOrphanNotes(index, folder);
|
|
8747
|
+
const orphans = allOrphans.slice(offset, offset + limit);
|
|
8748
|
+
return {
|
|
8749
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
8750
|
+
analysis: "orphans",
|
|
8751
|
+
orphan_count: allOrphans.length,
|
|
8752
|
+
returned_count: orphans.length,
|
|
8753
|
+
folder,
|
|
8754
|
+
orphans: orphans.map((o) => ({
|
|
8755
|
+
path: o.path,
|
|
8756
|
+
title: o.title,
|
|
8757
|
+
modified: o.modified.toISOString()
|
|
8758
|
+
}))
|
|
8439
8759
|
}, null, 2) }]
|
|
8440
8760
|
};
|
|
8441
8761
|
}
|
|
8442
|
-
case "
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
8446
|
-
error: "schema parameter is required for validate analysis"
|
|
8447
|
-
}, null, 2) }]
|
|
8448
|
-
};
|
|
8449
|
-
}
|
|
8450
|
-
const result = validateFrontmatter(
|
|
8451
|
-
index,
|
|
8452
|
-
params.schema,
|
|
8453
|
-
params.folder
|
|
8454
|
-
);
|
|
8762
|
+
case "dead_ends": {
|
|
8763
|
+
const allResults = findDeadEnds(index, folder, min_backlinks);
|
|
8764
|
+
const result = allResults.slice(offset, offset + limit);
|
|
8455
8765
|
return {
|
|
8456
8766
|
content: [{ type: "text", text: JSON.stringify({
|
|
8457
|
-
|
|
8458
|
-
|
|
8767
|
+
analysis: "dead_ends",
|
|
8768
|
+
criteria: { folder, min_backlinks },
|
|
8769
|
+
total_count: allResults.length,
|
|
8770
|
+
returned_count: result.length,
|
|
8771
|
+
dead_ends: result
|
|
8459
8772
|
}, null, 2) }]
|
|
8460
8773
|
};
|
|
8461
8774
|
}
|
|
8462
|
-
case "
|
|
8463
|
-
|
|
8775
|
+
case "sources": {
|
|
8776
|
+
const allResults = findSources(index, folder, min_outlinks);
|
|
8777
|
+
const result = allResults.slice(offset, offset + limit);
|
|
8778
|
+
return {
|
|
8779
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
8780
|
+
analysis: "sources",
|
|
8781
|
+
criteria: { folder, min_outlinks },
|
|
8782
|
+
total_count: allResults.length,
|
|
8783
|
+
returned_count: result.length,
|
|
8784
|
+
sources: result
|
|
8785
|
+
}, null, 2) }]
|
|
8786
|
+
};
|
|
8787
|
+
}
|
|
8788
|
+
case "hubs": {
|
|
8789
|
+
const allHubs = findHubNotes(index, min_links);
|
|
8790
|
+
const hubs = allHubs.slice(offset, offset + limit);
|
|
8791
|
+
return {
|
|
8792
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
8793
|
+
analysis: "hubs",
|
|
8794
|
+
hub_count: allHubs.length,
|
|
8795
|
+
returned_count: hubs.length,
|
|
8796
|
+
min_links,
|
|
8797
|
+
hubs: hubs.map((h) => ({
|
|
8798
|
+
path: h.path,
|
|
8799
|
+
title: h.title,
|
|
8800
|
+
backlink_count: h.backlink_count,
|
|
8801
|
+
forward_link_count: h.forward_link_count,
|
|
8802
|
+
total_connections: h.total_connections
|
|
8803
|
+
}))
|
|
8804
|
+
}, null, 2) }]
|
|
8805
|
+
};
|
|
8806
|
+
}
|
|
8807
|
+
case "stale": {
|
|
8808
|
+
if (days === void 0) {
|
|
8464
8809
|
return {
|
|
8465
8810
|
content: [{ type: "text", text: JSON.stringify({
|
|
8466
|
-
error: "
|
|
8811
|
+
error: "days parameter is required for stale analysis"
|
|
8467
8812
|
}, null, 2) }]
|
|
8468
8813
|
};
|
|
8469
8814
|
}
|
|
8470
|
-
const result =
|
|
8471
|
-
index,
|
|
8472
|
-
params.folder_schemas
|
|
8473
|
-
);
|
|
8815
|
+
const result = getStaleNotes(index, days, min_backlinks).slice(0, limit);
|
|
8474
8816
|
return {
|
|
8475
8817
|
content: [{ type: "text", text: JSON.stringify({
|
|
8476
|
-
|
|
8477
|
-
|
|
8818
|
+
analysis: "stale",
|
|
8819
|
+
criteria: { days, min_backlinks },
|
|
8820
|
+
count: result.length,
|
|
8821
|
+
notes: result.map((n) => ({
|
|
8822
|
+
...n,
|
|
8823
|
+
modified: n.modified.toISOString()
|
|
8824
|
+
}))
|
|
8478
8825
|
}, null, 2) }]
|
|
8479
8826
|
};
|
|
8480
8827
|
}
|
|
8481
|
-
case "
|
|
8482
|
-
const
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
params.min_confidence ?? 0.5
|
|
8828
|
+
case "immature": {
|
|
8829
|
+
const vaultPath2 = getVaultPath();
|
|
8830
|
+
const allNotes = Array.from(index.notes.values()).filter(
|
|
8831
|
+
(note) => !folder || note.path.startsWith(folder + "/") || note.path.substring(0, note.path.lastIndexOf("/")) === folder
|
|
8486
8832
|
);
|
|
8833
|
+
const conventions = inferFolderConventions(index, folder, 0.5);
|
|
8834
|
+
const expectedFields = conventions.inferred_fields.map((f) => f.name);
|
|
8835
|
+
const scored = allNotes.map((note) => {
|
|
8836
|
+
let wordCount = 0;
|
|
8837
|
+
try {
|
|
8838
|
+
const content = fs14.readFileSync(path13.join(vaultPath2, note.path), "utf-8");
|
|
8839
|
+
const body = content.replace(/^---[\s\S]*?---\n?/, "");
|
|
8840
|
+
wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
|
|
8841
|
+
} catch {
|
|
8842
|
+
}
|
|
8843
|
+
const wordScore = wordCount < 100 ? 0 : wordCount < 500 ? 0.5 : 1;
|
|
8844
|
+
const outlinkCount = note.outlinks.length;
|
|
8845
|
+
const outlinkScore = outlinkCount === 0 ? 0 : outlinkCount <= 3 ? 0.5 : 1;
|
|
8846
|
+
let frontmatterScore = 0;
|
|
8847
|
+
if (expectedFields.length > 0) {
|
|
8848
|
+
const existingFields = Object.keys(note.frontmatter);
|
|
8849
|
+
const presentCount = expectedFields.filter((f) => existingFields.includes(f)).length;
|
|
8850
|
+
frontmatterScore = presentCount / expectedFields.length;
|
|
8851
|
+
} else {
|
|
8852
|
+
frontmatterScore = 1;
|
|
8853
|
+
}
|
|
8854
|
+
const normalizedTitle = note.title.toLowerCase();
|
|
8855
|
+
const backlinks = index.backlinks.get(normalizedTitle) || [];
|
|
8856
|
+
const backlinkCount = backlinks.length;
|
|
8857
|
+
const backlinkScore = backlinkCount === 0 ? 0 : backlinkCount <= 2 ? 0.5 : 1;
|
|
8858
|
+
const maturity = (wordScore + outlinkScore + frontmatterScore + backlinkScore) / 4;
|
|
8859
|
+
return {
|
|
8860
|
+
path: note.path,
|
|
8861
|
+
title: note.title,
|
|
8862
|
+
maturity_score: Math.round(maturity * 100) / 100,
|
|
8863
|
+
components: {
|
|
8864
|
+
word_count: { value: wordCount, score: wordScore },
|
|
8865
|
+
outlinks: { value: outlinkCount, score: outlinkScore },
|
|
8866
|
+
frontmatter: { value: `${expectedFields.length > 0 ? Math.round(frontmatterScore * 100) : 100}%`, score: Math.round(frontmatterScore * 100) / 100 },
|
|
8867
|
+
backlinks: { value: backlinkCount, score: backlinkScore }
|
|
8868
|
+
},
|
|
8869
|
+
modified: note.modified.toISOString()
|
|
8870
|
+
};
|
|
8871
|
+
});
|
|
8872
|
+
scored.sort((a, b) => a.maturity_score - b.maturity_score);
|
|
8873
|
+
const total = scored.length;
|
|
8874
|
+
const paginated = scored.slice(offset, offset + limit);
|
|
8487
8875
|
return {
|
|
8488
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8876
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
8877
|
+
analysis: "immature",
|
|
8878
|
+
criteria: { folder: folder || null },
|
|
8879
|
+
total_count: total,
|
|
8880
|
+
returned_count: paginated.length,
|
|
8881
|
+
expected_fields: expectedFields,
|
|
8882
|
+
notes: paginated
|
|
8883
|
+
}, null, 2) }]
|
|
8489
8884
|
};
|
|
8490
8885
|
}
|
|
8491
|
-
case "
|
|
8492
|
-
const
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8886
|
+
case "evolution": {
|
|
8887
|
+
const db3 = getStateDb?.();
|
|
8888
|
+
if (!db3) {
|
|
8889
|
+
return {
|
|
8890
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
8891
|
+
error: "StateDb not available \u2014 graph evolution requires persistent state"
|
|
8892
|
+
}, null, 2) }]
|
|
8893
|
+
};
|
|
8894
|
+
}
|
|
8895
|
+
const daysBack = days ?? 30;
|
|
8896
|
+
const evolutions = getGraphEvolution(db3, daysBack);
|
|
8499
8897
|
return {
|
|
8500
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8898
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
8899
|
+
analysis: "evolution",
|
|
8900
|
+
days_back: daysBack,
|
|
8901
|
+
metrics: evolutions
|
|
8902
|
+
}, null, 2) }]
|
|
8501
8903
|
};
|
|
8502
8904
|
}
|
|
8503
|
-
case "
|
|
8504
|
-
|
|
8905
|
+
case "emerging_hubs": {
|
|
8906
|
+
const db3 = getStateDb?.();
|
|
8907
|
+
if (!db3) {
|
|
8505
8908
|
return {
|
|
8506
8909
|
content: [{ type: "text", text: JSON.stringify({
|
|
8507
|
-
error: "
|
|
8910
|
+
error: "StateDb not available \u2014 emerging hubs requires persistent state"
|
|
8508
8911
|
}, null, 2) }]
|
|
8509
8912
|
};
|
|
8510
8913
|
}
|
|
8511
|
-
const
|
|
8512
|
-
|
|
8513
|
-
existing_frontmatter: params.existing_frontmatter
|
|
8514
|
-
});
|
|
8914
|
+
const daysBack = days ?? 30;
|
|
8915
|
+
const hubs = getEmergingHubs(db3, daysBack);
|
|
8515
8916
|
return {
|
|
8516
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
8917
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
8918
|
+
analysis: "emerging_hubs",
|
|
8919
|
+
days_back: daysBack,
|
|
8920
|
+
count: hubs.length,
|
|
8921
|
+
hubs
|
|
8922
|
+
}, null, 2) }]
|
|
8517
8923
|
};
|
|
8518
8924
|
}
|
|
8519
8925
|
}
|
|
@@ -8521,45 +8927,392 @@ function registerVaultSchemaTools(server2, getIndex, getVaultPath) {
|
|
|
8521
8927
|
);
|
|
8522
8928
|
}
|
|
8523
8929
|
|
|
8524
|
-
// src/tools/read/
|
|
8525
|
-
import { z as
|
|
8930
|
+
// src/tools/read/vaultSchema.ts
|
|
8931
|
+
import { z as z9 } from "zod";
|
|
8526
8932
|
|
|
8527
|
-
// src/tools/read/
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
async function readFileContent2(notePath, vaultPath2) {
|
|
8535
|
-
const fullPath = path13.join(vaultPath2, notePath);
|
|
8536
|
-
try {
|
|
8537
|
-
return await fs13.readFile(fullPath, "utf-8");
|
|
8538
|
-
} catch {
|
|
8539
|
-
return null;
|
|
8540
|
-
}
|
|
8541
|
-
}
|
|
8542
|
-
function getBodyContent(content) {
|
|
8543
|
-
try {
|
|
8544
|
-
const parsed = matter3(content);
|
|
8545
|
-
const frontmatterMatch = content.match(/^---\n[\s\S]*?\n---\n?/);
|
|
8546
|
-
const bodyStartLine = frontmatterMatch ? frontmatterMatch[0].split("\n").length : 1;
|
|
8547
|
-
return { body: parsed.content, bodyStartLine };
|
|
8548
|
-
} catch {
|
|
8549
|
-
return { body: content, bodyStartLine: 1 };
|
|
8550
|
-
}
|
|
8551
|
-
}
|
|
8552
|
-
function removeCodeBlocks(content) {
|
|
8553
|
-
return content.replace(CODE_BLOCK_REGEX2, (match) => {
|
|
8554
|
-
const newlines = (match.match(/\n/g) || []).length;
|
|
8555
|
-
return "\n".repeat(newlines);
|
|
8556
|
-
});
|
|
8933
|
+
// src/tools/read/frontmatter.ts
|
|
8934
|
+
function getValueType2(value) {
|
|
8935
|
+
if (value === null) return "null";
|
|
8936
|
+
if (value === void 0) return "undefined";
|
|
8937
|
+
if (Array.isArray(value)) return "array";
|
|
8938
|
+
if (value instanceof Date) return "date";
|
|
8939
|
+
return typeof value;
|
|
8557
8940
|
}
|
|
8558
|
-
function
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8941
|
+
function getFrontmatterSchema(index) {
|
|
8942
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
8943
|
+
let notesWithFrontmatter = 0;
|
|
8944
|
+
for (const note of index.notes.values()) {
|
|
8945
|
+
const fm = note.frontmatter;
|
|
8946
|
+
if (!fm || Object.keys(fm).length === 0) continue;
|
|
8947
|
+
notesWithFrontmatter++;
|
|
8948
|
+
for (const [key, value] of Object.entries(fm)) {
|
|
8949
|
+
if (!fieldMap.has(key)) {
|
|
8950
|
+
fieldMap.set(key, {
|
|
8951
|
+
types: /* @__PURE__ */ new Set(),
|
|
8952
|
+
count: 0,
|
|
8953
|
+
examples: [],
|
|
8954
|
+
notes: []
|
|
8955
|
+
});
|
|
8956
|
+
}
|
|
8957
|
+
const info = fieldMap.get(key);
|
|
8958
|
+
info.count++;
|
|
8959
|
+
info.types.add(getValueType2(value));
|
|
8960
|
+
if (info.examples.length < 5) {
|
|
8961
|
+
const valueStr = JSON.stringify(value);
|
|
8962
|
+
const existingStrs = info.examples.map((e) => JSON.stringify(e));
|
|
8963
|
+
if (!existingStrs.includes(valueStr)) {
|
|
8964
|
+
info.examples.push(value);
|
|
8965
|
+
}
|
|
8966
|
+
}
|
|
8967
|
+
if (info.notes.length < 5) {
|
|
8968
|
+
info.notes.push(note.path);
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8971
|
+
}
|
|
8972
|
+
const fields = Array.from(fieldMap.entries()).map(([name, info]) => ({
|
|
8973
|
+
name,
|
|
8974
|
+
types: Array.from(info.types),
|
|
8975
|
+
count: info.count,
|
|
8976
|
+
examples: info.examples,
|
|
8977
|
+
notes_sample: info.notes
|
|
8978
|
+
})).sort((a, b) => b.count - a.count);
|
|
8979
|
+
return {
|
|
8980
|
+
total_notes: index.notes.size,
|
|
8981
|
+
notes_with_frontmatter: notesWithFrontmatter,
|
|
8982
|
+
field_count: fields.length,
|
|
8983
|
+
fields
|
|
8984
|
+
};
|
|
8985
|
+
}
|
|
8986
|
+
function getFieldValues(index, fieldName) {
|
|
8987
|
+
const valueMap = /* @__PURE__ */ new Map();
|
|
8988
|
+
let totalWithField = 0;
|
|
8989
|
+
for (const note of index.notes.values()) {
|
|
8990
|
+
const value = note.frontmatter[fieldName];
|
|
8991
|
+
if (value === void 0) continue;
|
|
8992
|
+
totalWithField++;
|
|
8993
|
+
const values = Array.isArray(value) ? value : [value];
|
|
8994
|
+
for (const v of values) {
|
|
8995
|
+
const key = JSON.stringify(v);
|
|
8996
|
+
if (!valueMap.has(key)) {
|
|
8997
|
+
valueMap.set(key, {
|
|
8998
|
+
value: v,
|
|
8999
|
+
count: 0,
|
|
9000
|
+
notes: []
|
|
9001
|
+
});
|
|
9002
|
+
}
|
|
9003
|
+
const info = valueMap.get(key);
|
|
9004
|
+
info.count++;
|
|
9005
|
+
info.notes.push(note.path);
|
|
9006
|
+
}
|
|
9007
|
+
}
|
|
9008
|
+
const valuesList = Array.from(valueMap.values()).sort((a, b) => b.count - a.count);
|
|
9009
|
+
return {
|
|
9010
|
+
field: fieldName,
|
|
9011
|
+
total_notes_with_field: totalWithField,
|
|
9012
|
+
unique_values: valuesList.length,
|
|
9013
|
+
values: valuesList
|
|
9014
|
+
};
|
|
9015
|
+
}
|
|
9016
|
+
function findFrontmatterInconsistencies(index) {
|
|
9017
|
+
const schema = getFrontmatterSchema(index);
|
|
9018
|
+
const inconsistencies = [];
|
|
9019
|
+
for (const field of schema.fields) {
|
|
9020
|
+
if (field.types.length > 1) {
|
|
9021
|
+
const examples = [];
|
|
9022
|
+
for (const note of index.notes.values()) {
|
|
9023
|
+
const value = note.frontmatter[field.name];
|
|
9024
|
+
if (value === void 0) continue;
|
|
9025
|
+
const type = getValueType2(value);
|
|
9026
|
+
if (!examples.some((e) => e.type === type)) {
|
|
9027
|
+
examples.push({
|
|
9028
|
+
type,
|
|
9029
|
+
value,
|
|
9030
|
+
note: note.path
|
|
9031
|
+
});
|
|
9032
|
+
}
|
|
9033
|
+
if (examples.length >= field.types.length) break;
|
|
9034
|
+
}
|
|
9035
|
+
inconsistencies.push({
|
|
9036
|
+
field: field.name,
|
|
9037
|
+
types_found: field.types,
|
|
9038
|
+
examples
|
|
9039
|
+
});
|
|
9040
|
+
}
|
|
9041
|
+
}
|
|
9042
|
+
return inconsistencies;
|
|
9043
|
+
}
|
|
9044
|
+
function validateFrontmatter(index, schema, folder) {
|
|
9045
|
+
const results = [];
|
|
9046
|
+
for (const note of index.notes.values()) {
|
|
9047
|
+
if (folder && !note.path.startsWith(folder)) continue;
|
|
9048
|
+
const issues = [];
|
|
9049
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema)) {
|
|
9050
|
+
const value = note.frontmatter[fieldName];
|
|
9051
|
+
if (fieldSchema.required && value === void 0) {
|
|
9052
|
+
issues.push({
|
|
9053
|
+
field: fieldName,
|
|
9054
|
+
issue: "missing",
|
|
9055
|
+
expected: "value required"
|
|
9056
|
+
});
|
|
9057
|
+
continue;
|
|
9058
|
+
}
|
|
9059
|
+
if (value === void 0) continue;
|
|
9060
|
+
if (fieldSchema.type) {
|
|
9061
|
+
const actualType = getValueType2(value);
|
|
9062
|
+
const allowedTypes = Array.isArray(fieldSchema.type) ? fieldSchema.type : [fieldSchema.type];
|
|
9063
|
+
if (!allowedTypes.includes(actualType)) {
|
|
9064
|
+
issues.push({
|
|
9065
|
+
field: fieldName,
|
|
9066
|
+
issue: "wrong_type",
|
|
9067
|
+
expected: allowedTypes.join(" | "),
|
|
9068
|
+
actual: actualType
|
|
9069
|
+
});
|
|
9070
|
+
}
|
|
9071
|
+
}
|
|
9072
|
+
if (fieldSchema.values) {
|
|
9073
|
+
const valueStr = JSON.stringify(value);
|
|
9074
|
+
const allowedStrs = fieldSchema.values.map((v) => JSON.stringify(v));
|
|
9075
|
+
if (!allowedStrs.includes(valueStr)) {
|
|
9076
|
+
issues.push({
|
|
9077
|
+
field: fieldName,
|
|
9078
|
+
issue: "invalid_value",
|
|
9079
|
+
expected: fieldSchema.values.map((v) => String(v)).join(" | "),
|
|
9080
|
+
actual: String(value)
|
|
9081
|
+
});
|
|
9082
|
+
}
|
|
9083
|
+
}
|
|
9084
|
+
}
|
|
9085
|
+
if (issues.length > 0) {
|
|
9086
|
+
results.push({
|
|
9087
|
+
path: note.path,
|
|
9088
|
+
issues
|
|
9089
|
+
});
|
|
9090
|
+
}
|
|
9091
|
+
}
|
|
9092
|
+
return results;
|
|
9093
|
+
}
|
|
9094
|
+
function findMissingFrontmatter(index, folderSchemas) {
|
|
9095
|
+
const results = [];
|
|
9096
|
+
for (const note of index.notes.values()) {
|
|
9097
|
+
for (const [folder, requiredFields] of Object.entries(folderSchemas)) {
|
|
9098
|
+
if (!note.path.startsWith(folder)) continue;
|
|
9099
|
+
const missing = requiredFields.filter(
|
|
9100
|
+
(field) => note.frontmatter[field] === void 0
|
|
9101
|
+
);
|
|
9102
|
+
if (missing.length > 0) {
|
|
9103
|
+
results.push({
|
|
9104
|
+
path: note.path,
|
|
9105
|
+
folder,
|
|
9106
|
+
missing_fields: missing
|
|
9107
|
+
});
|
|
9108
|
+
}
|
|
9109
|
+
}
|
|
9110
|
+
}
|
|
9111
|
+
return results;
|
|
9112
|
+
}
|
|
9113
|
+
|
|
9114
|
+
// src/tools/read/vaultSchema.ts
|
|
9115
|
+
function registerVaultSchemaTools(server2, getIndex, getVaultPath) {
|
|
9116
|
+
server2.registerTool(
|
|
9117
|
+
"vault_schema",
|
|
9118
|
+
{
|
|
9119
|
+
title: "Vault Schema",
|
|
9120
|
+
description: 'Analyze and validate vault frontmatter schema. Use analysis to pick the mode:\n- "overview": Schema of all frontmatter fields across the vault\n- "field_values": All unique values for a specific field\n- "inconsistencies": Fields with multiple types across notes\n- "validate": Validate notes against a provided schema\n- "missing": Find notes missing expected fields by folder\n- "conventions": Auto-detect metadata conventions for a folder\n- "incomplete": Find notes missing expected fields (inferred)\n- "suggest_values": Suggest values for a field based on usage\n- "contradictions": Find conflicting frontmatter values across notes referencing the same entity\n\nExample: vault_schema({ analysis: "field_values", field: "status" })\nExample: vault_schema({ analysis: "conventions", folder: "projects" })\nExample: vault_schema({ analysis: "contradictions", entity: "project alpha" })',
|
|
9121
|
+
inputSchema: {
|
|
9122
|
+
analysis: z9.enum([
|
|
9123
|
+
"overview",
|
|
9124
|
+
"field_values",
|
|
9125
|
+
"inconsistencies",
|
|
9126
|
+
"validate",
|
|
9127
|
+
"missing",
|
|
9128
|
+
"conventions",
|
|
9129
|
+
"incomplete",
|
|
9130
|
+
"suggest_values",
|
|
9131
|
+
"contradictions"
|
|
9132
|
+
]).describe("Type of schema analysis to perform"),
|
|
9133
|
+
field: z9.string().optional().describe("Field name (field_values, suggest_values)"),
|
|
9134
|
+
entity: z9.string().optional().describe("Entity name to scope contradiction detection to (contradictions mode)"),
|
|
9135
|
+
folder: z9.string().optional().describe("Folder to scope analysis to"),
|
|
9136
|
+
schema: z9.record(z9.object({
|
|
9137
|
+
required: z9.boolean().optional().describe("Whether field is required"),
|
|
9138
|
+
type: z9.union([z9.string(), z9.array(z9.string())]).optional().describe("Expected type(s)"),
|
|
9139
|
+
values: z9.array(z9.unknown()).optional().describe("Allowed values")
|
|
9140
|
+
})).optional().describe("Schema to validate against (validate mode)"),
|
|
9141
|
+
folder_schemas: z9.record(z9.array(z9.string())).optional().describe("Map of folder paths to required fields (missing mode)"),
|
|
9142
|
+
min_confidence: z9.coerce.number().min(0).max(1).optional().describe("Minimum confidence threshold (conventions)"),
|
|
9143
|
+
min_frequency: z9.coerce.number().min(0).max(1).optional().describe("Minimum field frequency (incomplete)"),
|
|
9144
|
+
existing_frontmatter: z9.record(z9.unknown()).optional().describe("Existing frontmatter for context (suggest_values)"),
|
|
9145
|
+
limit: z9.coerce.number().default(50).describe("Maximum results to return"),
|
|
9146
|
+
offset: z9.coerce.number().default(0).describe("Number of results to skip")
|
|
9147
|
+
}
|
|
9148
|
+
},
|
|
9149
|
+
async (params) => {
|
|
9150
|
+
requireIndex();
|
|
9151
|
+
const limit = Math.min(params.limit ?? 50, MAX_LIMIT);
|
|
9152
|
+
const index = getIndex();
|
|
9153
|
+
switch (params.analysis) {
|
|
9154
|
+
case "overview": {
|
|
9155
|
+
const result = getFrontmatterSchema(index);
|
|
9156
|
+
return {
|
|
9157
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9158
|
+
};
|
|
9159
|
+
}
|
|
9160
|
+
case "field_values": {
|
|
9161
|
+
if (!params.field) {
|
|
9162
|
+
return {
|
|
9163
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
9164
|
+
error: "field parameter is required for field_values analysis"
|
|
9165
|
+
}, null, 2) }]
|
|
9166
|
+
};
|
|
9167
|
+
}
|
|
9168
|
+
const result = getFieldValues(index, params.field);
|
|
9169
|
+
return {
|
|
9170
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9171
|
+
};
|
|
9172
|
+
}
|
|
9173
|
+
case "inconsistencies": {
|
|
9174
|
+
const result = findFrontmatterInconsistencies(index);
|
|
9175
|
+
return {
|
|
9176
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
9177
|
+
inconsistency_count: result.length,
|
|
9178
|
+
inconsistencies: result
|
|
9179
|
+
}, null, 2) }]
|
|
9180
|
+
};
|
|
9181
|
+
}
|
|
9182
|
+
case "validate": {
|
|
9183
|
+
if (!params.schema) {
|
|
9184
|
+
return {
|
|
9185
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
9186
|
+
error: "schema parameter is required for validate analysis"
|
|
9187
|
+
}, null, 2) }]
|
|
9188
|
+
};
|
|
9189
|
+
}
|
|
9190
|
+
const result = validateFrontmatter(
|
|
9191
|
+
index,
|
|
9192
|
+
params.schema,
|
|
9193
|
+
params.folder
|
|
9194
|
+
);
|
|
9195
|
+
return {
|
|
9196
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
9197
|
+
notes_with_issues: result.length,
|
|
9198
|
+
results: result
|
|
9199
|
+
}, null, 2) }]
|
|
9200
|
+
};
|
|
9201
|
+
}
|
|
9202
|
+
case "missing": {
|
|
9203
|
+
if (!params.folder_schemas) {
|
|
9204
|
+
return {
|
|
9205
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
9206
|
+
error: "folder_schemas parameter is required for missing analysis"
|
|
9207
|
+
}, null, 2) }]
|
|
9208
|
+
};
|
|
9209
|
+
}
|
|
9210
|
+
const result = findMissingFrontmatter(
|
|
9211
|
+
index,
|
|
9212
|
+
params.folder_schemas
|
|
9213
|
+
);
|
|
9214
|
+
return {
|
|
9215
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
9216
|
+
notes_with_missing_fields: result.length,
|
|
9217
|
+
results: result
|
|
9218
|
+
}, null, 2) }]
|
|
9219
|
+
};
|
|
9220
|
+
}
|
|
9221
|
+
case "conventions": {
|
|
9222
|
+
const result = inferFolderConventions(
|
|
9223
|
+
index,
|
|
9224
|
+
params.folder,
|
|
9225
|
+
params.min_confidence ?? 0.5
|
|
9226
|
+
);
|
|
9227
|
+
return {
|
|
9228
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9229
|
+
};
|
|
9230
|
+
}
|
|
9231
|
+
case "incomplete": {
|
|
9232
|
+
const result = findIncompleteNotes(
|
|
9233
|
+
index,
|
|
9234
|
+
params.folder,
|
|
9235
|
+
params.min_frequency ?? 0.7,
|
|
9236
|
+
limit,
|
|
9237
|
+
params.offset ?? 0
|
|
9238
|
+
);
|
|
9239
|
+
return {
|
|
9240
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9241
|
+
};
|
|
9242
|
+
}
|
|
9243
|
+
case "suggest_values": {
|
|
9244
|
+
if (!params.field) {
|
|
9245
|
+
return {
|
|
9246
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
9247
|
+
error: "field parameter is required for suggest_values analysis"
|
|
9248
|
+
}, null, 2) }]
|
|
9249
|
+
};
|
|
9250
|
+
}
|
|
9251
|
+
const result = suggestFieldValues(index, params.field, {
|
|
9252
|
+
folder: params.folder,
|
|
9253
|
+
existing_frontmatter: params.existing_frontmatter
|
|
9254
|
+
});
|
|
9255
|
+
return {
|
|
9256
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
9257
|
+
};
|
|
9258
|
+
}
|
|
9259
|
+
case "contradictions": {
|
|
9260
|
+
const allContradictions = findContradictions(index, params.entity);
|
|
9261
|
+
const paginated = allContradictions.slice(params.offset ?? 0, (params.offset ?? 0) + limit);
|
|
9262
|
+
return {
|
|
9263
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
9264
|
+
analysis: "contradictions",
|
|
9265
|
+
entity: params.entity || null,
|
|
9266
|
+
total_count: allContradictions.length,
|
|
9267
|
+
returned_count: paginated.length,
|
|
9268
|
+
contradictions: paginated
|
|
9269
|
+
}, null, 2) }]
|
|
9270
|
+
};
|
|
9271
|
+
}
|
|
9272
|
+
}
|
|
9273
|
+
}
|
|
9274
|
+
);
|
|
9275
|
+
}
|
|
9276
|
+
|
|
9277
|
+
// src/tools/read/noteIntelligence.ts
|
|
9278
|
+
import { z as z10 } from "zod";
|
|
9279
|
+
|
|
9280
|
+
// src/tools/read/bidirectional.ts
|
|
9281
|
+
import * as fs15 from "fs/promises";
|
|
9282
|
+
import * as path14 from "path";
|
|
9283
|
+
import matter3 from "gray-matter";
|
|
9284
|
+
var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
|
|
9285
|
+
var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
|
|
9286
|
+
var WIKILINK_REGEX2 = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]+)?\]\]/g;
|
|
9287
|
+
async function readFileContent2(notePath, vaultPath2) {
|
|
9288
|
+
const fullPath = path14.join(vaultPath2, notePath);
|
|
9289
|
+
try {
|
|
9290
|
+
return await fs15.readFile(fullPath, "utf-8");
|
|
9291
|
+
} catch {
|
|
9292
|
+
return null;
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
function getBodyContent(content) {
|
|
9296
|
+
try {
|
|
9297
|
+
const parsed = matter3(content);
|
|
9298
|
+
const frontmatterMatch = content.match(/^---\n[\s\S]*?\n---\n?/);
|
|
9299
|
+
const bodyStartLine = frontmatterMatch ? frontmatterMatch[0].split("\n").length : 1;
|
|
9300
|
+
return { body: parsed.content, bodyStartLine };
|
|
9301
|
+
} catch {
|
|
9302
|
+
return { body: content, bodyStartLine: 1 };
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9305
|
+
function removeCodeBlocks(content) {
|
|
9306
|
+
return content.replace(CODE_BLOCK_REGEX2, (match) => {
|
|
9307
|
+
const newlines = (match.match(/\n/g) || []).length;
|
|
9308
|
+
return "\n".repeat(newlines);
|
|
9309
|
+
});
|
|
9310
|
+
}
|
|
9311
|
+
function extractWikilinksFromValue(value) {
|
|
9312
|
+
if (typeof value === "string") {
|
|
9313
|
+
const matches = [];
|
|
9314
|
+
let match;
|
|
9315
|
+
WIKILINK_REGEX2.lastIndex = 0;
|
|
8563
9316
|
while ((match = WIKILINK_REGEX2.exec(value)) !== null) {
|
|
8564
9317
|
matches.push(match[1].trim());
|
|
8565
9318
|
}
|
|
@@ -8801,21 +9554,21 @@ async function validateCrossLayer(index, notePath, vaultPath2) {
|
|
|
8801
9554
|
}
|
|
8802
9555
|
|
|
8803
9556
|
// src/tools/read/computed.ts
|
|
8804
|
-
import * as
|
|
8805
|
-
import * as
|
|
9557
|
+
import * as fs16 from "fs/promises";
|
|
9558
|
+
import * as path15 from "path";
|
|
8806
9559
|
import matter4 from "gray-matter";
|
|
8807
9560
|
async function readFileContent3(notePath, vaultPath2) {
|
|
8808
|
-
const fullPath =
|
|
9561
|
+
const fullPath = path15.join(vaultPath2, notePath);
|
|
8809
9562
|
try {
|
|
8810
|
-
return await
|
|
9563
|
+
return await fs16.readFile(fullPath, "utf-8");
|
|
8811
9564
|
} catch {
|
|
8812
9565
|
return null;
|
|
8813
9566
|
}
|
|
8814
9567
|
}
|
|
8815
9568
|
async function getFileStats(notePath, vaultPath2) {
|
|
8816
|
-
const fullPath =
|
|
9569
|
+
const fullPath = path15.join(vaultPath2, notePath);
|
|
8817
9570
|
try {
|
|
8818
|
-
const stats = await
|
|
9571
|
+
const stats = await fs16.stat(fullPath);
|
|
8819
9572
|
return {
|
|
8820
9573
|
modified: stats.mtime,
|
|
8821
9574
|
created: stats.birthtime
|
|
@@ -9025,8 +9778,8 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
|
|
|
9025
9778
|
// src/tools/write/mutations.ts
|
|
9026
9779
|
init_writer();
|
|
9027
9780
|
import { z as z11 } from "zod";
|
|
9028
|
-
import
|
|
9029
|
-
import
|
|
9781
|
+
import fs19 from "fs/promises";
|
|
9782
|
+
import path18 from "path";
|
|
9030
9783
|
|
|
9031
9784
|
// src/core/write/validator.ts
|
|
9032
9785
|
var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
|
|
@@ -9228,8 +9981,8 @@ function runValidationPipeline(content, format, options = {}) {
|
|
|
9228
9981
|
|
|
9229
9982
|
// src/core/write/mutation-helpers.ts
|
|
9230
9983
|
init_writer();
|
|
9231
|
-
import
|
|
9232
|
-
import
|
|
9984
|
+
import fs18 from "fs/promises";
|
|
9985
|
+
import path17 from "path";
|
|
9233
9986
|
init_constants();
|
|
9234
9987
|
init_writer();
|
|
9235
9988
|
function formatMcpResult(result) {
|
|
@@ -9278,9 +10031,9 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
|
|
|
9278
10031
|
return info;
|
|
9279
10032
|
}
|
|
9280
10033
|
async function ensureFileExists(vaultPath2, notePath) {
|
|
9281
|
-
const fullPath =
|
|
10034
|
+
const fullPath = path17.join(vaultPath2, notePath);
|
|
9282
10035
|
try {
|
|
9283
|
-
await
|
|
10036
|
+
await fs18.access(fullPath);
|
|
9284
10037
|
return null;
|
|
9285
10038
|
} catch {
|
|
9286
10039
|
return errorResult(notePath, `File not found: ${notePath}`);
|
|
@@ -9378,10 +10131,10 @@ async function withVaultFrontmatter(options, operation) {
|
|
|
9378
10131
|
|
|
9379
10132
|
// src/tools/write/mutations.ts
|
|
9380
10133
|
async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
9381
|
-
const fullPath =
|
|
9382
|
-
await
|
|
10134
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
10135
|
+
await fs19.mkdir(path18.dirname(fullPath), { recursive: true });
|
|
9383
10136
|
const templates = config.templates || {};
|
|
9384
|
-
const filename =
|
|
10137
|
+
const filename = path18.basename(notePath, ".md").toLowerCase();
|
|
9385
10138
|
let templatePath;
|
|
9386
10139
|
const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
|
|
9387
10140
|
const weeklyPattern = /^\d{4}-W\d{2}/;
|
|
@@ -9402,10 +10155,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
9402
10155
|
let templateContent;
|
|
9403
10156
|
if (templatePath) {
|
|
9404
10157
|
try {
|
|
9405
|
-
const absTemplatePath =
|
|
9406
|
-
templateContent = await
|
|
10158
|
+
const absTemplatePath = path18.join(vaultPath2, templatePath);
|
|
10159
|
+
templateContent = await fs19.readFile(absTemplatePath, "utf-8");
|
|
9407
10160
|
} catch {
|
|
9408
|
-
const title =
|
|
10161
|
+
const title = path18.basename(notePath, ".md");
|
|
9409
10162
|
templateContent = `---
|
|
9410
10163
|
---
|
|
9411
10164
|
|
|
@@ -9414,7 +10167,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
9414
10167
|
templatePath = void 0;
|
|
9415
10168
|
}
|
|
9416
10169
|
} else {
|
|
9417
|
-
const title =
|
|
10170
|
+
const title = path18.basename(notePath, ".md");
|
|
9418
10171
|
templateContent = `---
|
|
9419
10172
|
---
|
|
9420
10173
|
|
|
@@ -9423,8 +10176,8 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
9423
10176
|
}
|
|
9424
10177
|
const now = /* @__PURE__ */ new Date();
|
|
9425
10178
|
const dateStr = now.toISOString().split("T")[0];
|
|
9426
|
-
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g,
|
|
9427
|
-
await
|
|
10179
|
+
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path18.basename(notePath, ".md"));
|
|
10180
|
+
await fs19.writeFile(fullPath, templateContent, "utf-8");
|
|
9428
10181
|
return { created: true, templateUsed: templatePath };
|
|
9429
10182
|
}
|
|
9430
10183
|
function registerMutationTools(server2, vaultPath2, getConfig = () => ({})) {
|
|
@@ -9455,9 +10208,9 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
9455
10208
|
let noteCreated = false;
|
|
9456
10209
|
let templateUsed;
|
|
9457
10210
|
if (create_if_missing) {
|
|
9458
|
-
const fullPath =
|
|
10211
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
9459
10212
|
try {
|
|
9460
|
-
await
|
|
10213
|
+
await fs19.access(fullPath);
|
|
9461
10214
|
} catch {
|
|
9462
10215
|
const config = getConfig();
|
|
9463
10216
|
const result = await createNoteFromTemplate(vaultPath2, notePath, config);
|
|
@@ -9892,8 +10645,8 @@ Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { st
|
|
|
9892
10645
|
// src/tools/write/notes.ts
|
|
9893
10646
|
init_writer();
|
|
9894
10647
|
import { z as z14 } from "zod";
|
|
9895
|
-
import
|
|
9896
|
-
import
|
|
10648
|
+
import fs20 from "fs/promises";
|
|
10649
|
+
import path19 from "path";
|
|
9897
10650
|
function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
9898
10651
|
server2.tool(
|
|
9899
10652
|
"vault_create_note",
|
|
@@ -9916,23 +10669,23 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9916
10669
|
if (!validatePath(vaultPath2, notePath)) {
|
|
9917
10670
|
return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
|
|
9918
10671
|
}
|
|
9919
|
-
const fullPath =
|
|
10672
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
9920
10673
|
const existsCheck = await ensureFileExists(vaultPath2, notePath);
|
|
9921
10674
|
if (existsCheck === null && !overwrite) {
|
|
9922
10675
|
return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
|
|
9923
10676
|
}
|
|
9924
|
-
const dir =
|
|
9925
|
-
await
|
|
10677
|
+
const dir = path19.dirname(fullPath);
|
|
10678
|
+
await fs20.mkdir(dir, { recursive: true });
|
|
9926
10679
|
let effectiveContent = content;
|
|
9927
10680
|
let effectiveFrontmatter = frontmatter;
|
|
9928
10681
|
if (template) {
|
|
9929
|
-
const templatePath =
|
|
10682
|
+
const templatePath = path19.join(vaultPath2, template);
|
|
9930
10683
|
try {
|
|
9931
|
-
const raw = await
|
|
10684
|
+
const raw = await fs20.readFile(templatePath, "utf-8");
|
|
9932
10685
|
const matter9 = (await import("gray-matter")).default;
|
|
9933
10686
|
const parsed = matter9(raw);
|
|
9934
10687
|
const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
9935
|
-
const title =
|
|
10688
|
+
const title = path19.basename(notePath, ".md");
|
|
9936
10689
|
let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
|
|
9937
10690
|
if (content) {
|
|
9938
10691
|
templateContent = templateContent.trimEnd() + "\n\n" + content;
|
|
@@ -9944,7 +10697,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9944
10697
|
}
|
|
9945
10698
|
}
|
|
9946
10699
|
const warnings = [];
|
|
9947
|
-
const noteName =
|
|
10700
|
+
const noteName = path19.basename(notePath, ".md");
|
|
9948
10701
|
const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
|
|
9949
10702
|
const preflight = checkPreflightSimilarity(noteName);
|
|
9950
10703
|
if (preflight.existingEntity) {
|
|
@@ -10061,8 +10814,8 @@ ${sources}`;
|
|
|
10061
10814
|
}
|
|
10062
10815
|
return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
|
|
10063
10816
|
}
|
|
10064
|
-
const fullPath =
|
|
10065
|
-
await
|
|
10817
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
10818
|
+
await fs20.unlink(fullPath);
|
|
10066
10819
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
|
|
10067
10820
|
const message = backlinkWarning ? `Deleted note: ${notePath}
|
|
10068
10821
|
|
|
@@ -10080,8 +10833,8 @@ Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
|
|
|
10080
10833
|
// src/tools/write/move-notes.ts
|
|
10081
10834
|
init_writer();
|
|
10082
10835
|
import { z as z15 } from "zod";
|
|
10083
|
-
import
|
|
10084
|
-
import
|
|
10836
|
+
import fs21 from "fs/promises";
|
|
10837
|
+
import path20 from "path";
|
|
10085
10838
|
import matter6 from "gray-matter";
|
|
10086
10839
|
function escapeRegex(str) {
|
|
10087
10840
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -10100,16 +10853,16 @@ function extractWikilinks2(content) {
|
|
|
10100
10853
|
return wikilinks;
|
|
10101
10854
|
}
|
|
10102
10855
|
function getTitleFromPath(filePath) {
|
|
10103
|
-
return
|
|
10856
|
+
return path20.basename(filePath, ".md");
|
|
10104
10857
|
}
|
|
10105
10858
|
async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
10106
10859
|
const results = [];
|
|
10107
10860
|
const allTargets = [targetTitle, ...targetAliases].map((t) => t.toLowerCase());
|
|
10108
10861
|
async function scanDir(dir) {
|
|
10109
10862
|
const files = [];
|
|
10110
|
-
const entries = await
|
|
10863
|
+
const entries = await fs21.readdir(dir, { withFileTypes: true });
|
|
10111
10864
|
for (const entry of entries) {
|
|
10112
|
-
const fullPath =
|
|
10865
|
+
const fullPath = path20.join(dir, entry.name);
|
|
10113
10866
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
10114
10867
|
files.push(...await scanDir(fullPath));
|
|
10115
10868
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -10120,8 +10873,8 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
10120
10873
|
}
|
|
10121
10874
|
const allFiles = await scanDir(vaultPath2);
|
|
10122
10875
|
for (const filePath of allFiles) {
|
|
10123
|
-
const relativePath =
|
|
10124
|
-
const content = await
|
|
10876
|
+
const relativePath = path20.relative(vaultPath2, filePath);
|
|
10877
|
+
const content = await fs21.readFile(filePath, "utf-8");
|
|
10125
10878
|
const wikilinks = extractWikilinks2(content);
|
|
10126
10879
|
const matchingLinks = [];
|
|
10127
10880
|
for (const link of wikilinks) {
|
|
@@ -10140,8 +10893,8 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
10140
10893
|
return results;
|
|
10141
10894
|
}
|
|
10142
10895
|
async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
|
|
10143
|
-
const fullPath =
|
|
10144
|
-
const raw = await
|
|
10896
|
+
const fullPath = path20.join(vaultPath2, filePath);
|
|
10897
|
+
const raw = await fs21.readFile(fullPath, "utf-8");
|
|
10145
10898
|
const parsed = matter6(raw);
|
|
10146
10899
|
let content = parsed.content;
|
|
10147
10900
|
let totalUpdated = 0;
|
|
@@ -10207,10 +10960,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10207
10960
|
};
|
|
10208
10961
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10209
10962
|
}
|
|
10210
|
-
const oldFullPath =
|
|
10211
|
-
const newFullPath =
|
|
10963
|
+
const oldFullPath = path20.join(vaultPath2, oldPath);
|
|
10964
|
+
const newFullPath = path20.join(vaultPath2, newPath);
|
|
10212
10965
|
try {
|
|
10213
|
-
await
|
|
10966
|
+
await fs21.access(oldFullPath);
|
|
10214
10967
|
} catch {
|
|
10215
10968
|
const result2 = {
|
|
10216
10969
|
success: false,
|
|
@@ -10220,7 +10973,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10220
10973
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10221
10974
|
}
|
|
10222
10975
|
try {
|
|
10223
|
-
await
|
|
10976
|
+
await fs21.access(newFullPath);
|
|
10224
10977
|
const result2 = {
|
|
10225
10978
|
success: false,
|
|
10226
10979
|
message: `Destination already exists: ${newPath}`,
|
|
@@ -10229,7 +10982,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10229
10982
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10230
10983
|
} catch {
|
|
10231
10984
|
}
|
|
10232
|
-
const sourceContent = await
|
|
10985
|
+
const sourceContent = await fs21.readFile(oldFullPath, "utf-8");
|
|
10233
10986
|
const parsed = matter6(sourceContent);
|
|
10234
10987
|
const aliases = extractAliases2(parsed.data);
|
|
10235
10988
|
const oldTitle = getTitleFromPath(oldPath);
|
|
@@ -10258,9 +11011,9 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10258
11011
|
}
|
|
10259
11012
|
}
|
|
10260
11013
|
}
|
|
10261
|
-
const destDir =
|
|
10262
|
-
await
|
|
10263
|
-
await
|
|
11014
|
+
const destDir = path20.dirname(newFullPath);
|
|
11015
|
+
await fs21.mkdir(destDir, { recursive: true });
|
|
11016
|
+
await fs21.rename(oldFullPath, newFullPath);
|
|
10264
11017
|
let gitCommit;
|
|
10265
11018
|
let undoAvailable;
|
|
10266
11019
|
let staleLockDetected;
|
|
@@ -10344,12 +11097,12 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10344
11097
|
if (sanitizedTitle !== newTitle) {
|
|
10345
11098
|
console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
|
|
10346
11099
|
}
|
|
10347
|
-
const fullPath =
|
|
10348
|
-
const dir =
|
|
10349
|
-
const newPath = dir === "." ? `${sanitizedTitle}.md` :
|
|
10350
|
-
const newFullPath =
|
|
11100
|
+
const fullPath = path20.join(vaultPath2, notePath);
|
|
11101
|
+
const dir = path20.dirname(notePath);
|
|
11102
|
+
const newPath = dir === "." ? `${sanitizedTitle}.md` : path20.join(dir, `${sanitizedTitle}.md`);
|
|
11103
|
+
const newFullPath = path20.join(vaultPath2, newPath);
|
|
10351
11104
|
try {
|
|
10352
|
-
await
|
|
11105
|
+
await fs21.access(fullPath);
|
|
10353
11106
|
} catch {
|
|
10354
11107
|
const result2 = {
|
|
10355
11108
|
success: false,
|
|
@@ -10360,7 +11113,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10360
11113
|
}
|
|
10361
11114
|
if (fullPath !== newFullPath) {
|
|
10362
11115
|
try {
|
|
10363
|
-
await
|
|
11116
|
+
await fs21.access(newFullPath);
|
|
10364
11117
|
const result2 = {
|
|
10365
11118
|
success: false,
|
|
10366
11119
|
message: `A note with this title already exists: ${newPath}`,
|
|
@@ -10370,7 +11123,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10370
11123
|
} catch {
|
|
10371
11124
|
}
|
|
10372
11125
|
}
|
|
10373
|
-
const sourceContent = await
|
|
11126
|
+
const sourceContent = await fs21.readFile(fullPath, "utf-8");
|
|
10374
11127
|
const parsed = matter6(sourceContent);
|
|
10375
11128
|
const aliases = extractAliases2(parsed.data);
|
|
10376
11129
|
const oldTitle = getTitleFromPath(notePath);
|
|
@@ -10399,7 +11152,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10399
11152
|
}
|
|
10400
11153
|
}
|
|
10401
11154
|
if (fullPath !== newFullPath) {
|
|
10402
|
-
await
|
|
11155
|
+
await fs21.rename(fullPath, newFullPath);
|
|
10403
11156
|
}
|
|
10404
11157
|
let gitCommit;
|
|
10405
11158
|
let undoAvailable;
|
|
@@ -10572,8 +11325,8 @@ init_schema();
|
|
|
10572
11325
|
|
|
10573
11326
|
// src/core/write/policy/parser.ts
|
|
10574
11327
|
init_schema();
|
|
10575
|
-
import
|
|
10576
|
-
import
|
|
11328
|
+
import fs22 from "fs/promises";
|
|
11329
|
+
import path21 from "path";
|
|
10577
11330
|
import matter7 from "gray-matter";
|
|
10578
11331
|
function parseYaml(content) {
|
|
10579
11332
|
const parsed = matter7(`---
|
|
@@ -10598,7 +11351,7 @@ function parsePolicyString(yamlContent) {
|
|
|
10598
11351
|
}
|
|
10599
11352
|
async function loadPolicyFile(filePath) {
|
|
10600
11353
|
try {
|
|
10601
|
-
const content = await
|
|
11354
|
+
const content = await fs22.readFile(filePath, "utf-8");
|
|
10602
11355
|
return parsePolicyString(content);
|
|
10603
11356
|
} catch (error) {
|
|
10604
11357
|
if (error.code === "ENOENT") {
|
|
@@ -10622,15 +11375,15 @@ async function loadPolicyFile(filePath) {
|
|
|
10622
11375
|
}
|
|
10623
11376
|
}
|
|
10624
11377
|
async function loadPolicy(vaultPath2, policyName) {
|
|
10625
|
-
const policiesDir =
|
|
10626
|
-
const policyPath =
|
|
11378
|
+
const policiesDir = path21.join(vaultPath2, ".claude", "policies");
|
|
11379
|
+
const policyPath = path21.join(policiesDir, `${policyName}.yaml`);
|
|
10627
11380
|
try {
|
|
10628
|
-
await
|
|
11381
|
+
await fs22.access(policyPath);
|
|
10629
11382
|
return loadPolicyFile(policyPath);
|
|
10630
11383
|
} catch {
|
|
10631
|
-
const ymlPath =
|
|
11384
|
+
const ymlPath = path21.join(policiesDir, `${policyName}.yml`);
|
|
10632
11385
|
try {
|
|
10633
|
-
await
|
|
11386
|
+
await fs22.access(ymlPath);
|
|
10634
11387
|
return loadPolicyFile(ymlPath);
|
|
10635
11388
|
} catch {
|
|
10636
11389
|
return {
|
|
@@ -10768,8 +11521,8 @@ init_template();
|
|
|
10768
11521
|
init_conditions();
|
|
10769
11522
|
init_schema();
|
|
10770
11523
|
init_writer();
|
|
10771
|
-
import
|
|
10772
|
-
import
|
|
11524
|
+
import fs24 from "fs/promises";
|
|
11525
|
+
import path23 from "path";
|
|
10773
11526
|
init_constants();
|
|
10774
11527
|
async function executeStep(step, vaultPath2, context, conditionResults) {
|
|
10775
11528
|
const { execute, reason } = shouldStepExecute(step.when, conditionResults);
|
|
@@ -10838,9 +11591,9 @@ async function executeAddToSection(params, vaultPath2, context) {
|
|
|
10838
11591
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
10839
11592
|
const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
|
|
10840
11593
|
const maxSuggestions = Number(params.maxSuggestions) || 3;
|
|
10841
|
-
const fullPath =
|
|
11594
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
10842
11595
|
try {
|
|
10843
|
-
await
|
|
11596
|
+
await fs24.access(fullPath);
|
|
10844
11597
|
} catch {
|
|
10845
11598
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
10846
11599
|
}
|
|
@@ -10878,9 +11631,9 @@ async function executeRemoveFromSection(params, vaultPath2) {
|
|
|
10878
11631
|
const pattern = String(params.pattern || "");
|
|
10879
11632
|
const mode = params.mode || "first";
|
|
10880
11633
|
const useRegex = Boolean(params.useRegex);
|
|
10881
|
-
const fullPath =
|
|
11634
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
10882
11635
|
try {
|
|
10883
|
-
await
|
|
11636
|
+
await fs24.access(fullPath);
|
|
10884
11637
|
} catch {
|
|
10885
11638
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
10886
11639
|
}
|
|
@@ -10909,9 +11662,9 @@ async function executeReplaceInSection(params, vaultPath2, context) {
|
|
|
10909
11662
|
const mode = params.mode || "first";
|
|
10910
11663
|
const useRegex = Boolean(params.useRegex);
|
|
10911
11664
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
10912
|
-
const fullPath =
|
|
11665
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
10913
11666
|
try {
|
|
10914
|
-
await
|
|
11667
|
+
await fs24.access(fullPath);
|
|
10915
11668
|
} catch {
|
|
10916
11669
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
10917
11670
|
}
|
|
@@ -10952,16 +11705,16 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
10952
11705
|
if (!validatePath(vaultPath2, notePath)) {
|
|
10953
11706
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
10954
11707
|
}
|
|
10955
|
-
const fullPath =
|
|
11708
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
10956
11709
|
try {
|
|
10957
|
-
await
|
|
11710
|
+
await fs24.access(fullPath);
|
|
10958
11711
|
if (!overwrite) {
|
|
10959
11712
|
return { success: false, message: `File already exists: ${notePath}`, path: notePath };
|
|
10960
11713
|
}
|
|
10961
11714
|
} catch {
|
|
10962
11715
|
}
|
|
10963
|
-
const dir =
|
|
10964
|
-
await
|
|
11716
|
+
const dir = path23.dirname(fullPath);
|
|
11717
|
+
await fs24.mkdir(dir, { recursive: true });
|
|
10965
11718
|
const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
|
|
10966
11719
|
await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
|
|
10967
11720
|
return {
|
|
@@ -10980,13 +11733,13 @@ async function executeDeleteNote(params, vaultPath2) {
|
|
|
10980
11733
|
if (!validatePath(vaultPath2, notePath)) {
|
|
10981
11734
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
10982
11735
|
}
|
|
10983
|
-
const fullPath =
|
|
11736
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
10984
11737
|
try {
|
|
10985
|
-
await
|
|
11738
|
+
await fs24.access(fullPath);
|
|
10986
11739
|
} catch {
|
|
10987
11740
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
10988
11741
|
}
|
|
10989
|
-
await
|
|
11742
|
+
await fs24.unlink(fullPath);
|
|
10990
11743
|
return {
|
|
10991
11744
|
success: true,
|
|
10992
11745
|
message: `Deleted note: ${notePath}`,
|
|
@@ -10997,9 +11750,9 @@ async function executeToggleTask(params, vaultPath2) {
|
|
|
10997
11750
|
const notePath = String(params.path || "");
|
|
10998
11751
|
const task = String(params.task || "");
|
|
10999
11752
|
const section = params.section ? String(params.section) : void 0;
|
|
11000
|
-
const fullPath =
|
|
11753
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11001
11754
|
try {
|
|
11002
|
-
await
|
|
11755
|
+
await fs24.access(fullPath);
|
|
11003
11756
|
} catch {
|
|
11004
11757
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11005
11758
|
}
|
|
@@ -11040,9 +11793,9 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
11040
11793
|
const completed = Boolean(params.completed);
|
|
11041
11794
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
11042
11795
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
11043
|
-
const fullPath =
|
|
11796
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11044
11797
|
try {
|
|
11045
|
-
await
|
|
11798
|
+
await fs24.access(fullPath);
|
|
11046
11799
|
} catch {
|
|
11047
11800
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11048
11801
|
}
|
|
@@ -11077,9 +11830,9 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
11077
11830
|
async function executeUpdateFrontmatter(params, vaultPath2) {
|
|
11078
11831
|
const notePath = String(params.path || "");
|
|
11079
11832
|
const updates = params.frontmatter || {};
|
|
11080
|
-
const fullPath =
|
|
11833
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11081
11834
|
try {
|
|
11082
|
-
await
|
|
11835
|
+
await fs24.access(fullPath);
|
|
11083
11836
|
} catch {
|
|
11084
11837
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11085
11838
|
}
|
|
@@ -11099,9 +11852,9 @@ async function executeAddFrontmatterField(params, vaultPath2) {
|
|
|
11099
11852
|
const notePath = String(params.path || "");
|
|
11100
11853
|
const key = String(params.key || "");
|
|
11101
11854
|
const value = params.value;
|
|
11102
|
-
const fullPath =
|
|
11855
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11103
11856
|
try {
|
|
11104
|
-
await
|
|
11857
|
+
await fs24.access(fullPath);
|
|
11105
11858
|
} catch {
|
|
11106
11859
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11107
11860
|
}
|
|
@@ -11259,15 +12012,15 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
|
|
|
11259
12012
|
async function rollbackChanges(vaultPath2, originalContents, filesModified) {
|
|
11260
12013
|
for (const filePath of filesModified) {
|
|
11261
12014
|
const original = originalContents.get(filePath);
|
|
11262
|
-
const fullPath =
|
|
12015
|
+
const fullPath = path23.join(vaultPath2, filePath);
|
|
11263
12016
|
if (original === null) {
|
|
11264
12017
|
try {
|
|
11265
|
-
await
|
|
12018
|
+
await fs24.unlink(fullPath);
|
|
11266
12019
|
} catch {
|
|
11267
12020
|
}
|
|
11268
12021
|
} else if (original !== void 0) {
|
|
11269
12022
|
try {
|
|
11270
|
-
await
|
|
12023
|
+
await fs24.writeFile(fullPath, original);
|
|
11271
12024
|
} catch {
|
|
11272
12025
|
}
|
|
11273
12026
|
}
|
|
@@ -11313,27 +12066,27 @@ async function previewPolicy(policy, vaultPath2, variables) {
|
|
|
11313
12066
|
}
|
|
11314
12067
|
|
|
11315
12068
|
// src/core/write/policy/storage.ts
|
|
11316
|
-
import
|
|
11317
|
-
import
|
|
12069
|
+
import fs25 from "fs/promises";
|
|
12070
|
+
import path24 from "path";
|
|
11318
12071
|
function getPoliciesDir(vaultPath2) {
|
|
11319
|
-
return
|
|
12072
|
+
return path24.join(vaultPath2, ".claude", "policies");
|
|
11320
12073
|
}
|
|
11321
12074
|
async function ensurePoliciesDir(vaultPath2) {
|
|
11322
12075
|
const dir = getPoliciesDir(vaultPath2);
|
|
11323
|
-
await
|
|
12076
|
+
await fs25.mkdir(dir, { recursive: true });
|
|
11324
12077
|
}
|
|
11325
12078
|
async function listPolicies(vaultPath2) {
|
|
11326
12079
|
const dir = getPoliciesDir(vaultPath2);
|
|
11327
12080
|
const policies = [];
|
|
11328
12081
|
try {
|
|
11329
|
-
const files = await
|
|
12082
|
+
const files = await fs25.readdir(dir);
|
|
11330
12083
|
for (const file of files) {
|
|
11331
12084
|
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
|
|
11332
12085
|
continue;
|
|
11333
12086
|
}
|
|
11334
|
-
const filePath =
|
|
11335
|
-
const stat3 = await
|
|
11336
|
-
const content = await
|
|
12087
|
+
const filePath = path24.join(dir, file);
|
|
12088
|
+
const stat3 = await fs25.stat(filePath);
|
|
12089
|
+
const content = await fs25.readFile(filePath, "utf-8");
|
|
11337
12090
|
const metadata = extractPolicyMetadata(content);
|
|
11338
12091
|
policies.push({
|
|
11339
12092
|
name: metadata.name || file.replace(/\.ya?ml$/, ""),
|
|
@@ -11356,10 +12109,10 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
|
|
|
11356
12109
|
const dir = getPoliciesDir(vaultPath2);
|
|
11357
12110
|
await ensurePoliciesDir(vaultPath2);
|
|
11358
12111
|
const filename = `${policyName}.yaml`;
|
|
11359
|
-
const filePath =
|
|
12112
|
+
const filePath = path24.join(dir, filename);
|
|
11360
12113
|
if (!overwrite) {
|
|
11361
12114
|
try {
|
|
11362
|
-
await
|
|
12115
|
+
await fs25.access(filePath);
|
|
11363
12116
|
return {
|
|
11364
12117
|
success: false,
|
|
11365
12118
|
path: filename,
|
|
@@ -11376,7 +12129,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
|
|
|
11376
12129
|
message: `Invalid policy: ${validation.errors.map((e) => e.message).join("; ")}`
|
|
11377
12130
|
};
|
|
11378
12131
|
}
|
|
11379
|
-
await
|
|
12132
|
+
await fs25.writeFile(filePath, content, "utf-8");
|
|
11380
12133
|
return {
|
|
11381
12134
|
success: true,
|
|
11382
12135
|
path: filename,
|
|
@@ -11899,8 +12652,8 @@ function registerPolicyTools(server2, vaultPath2) {
|
|
|
11899
12652
|
import { z as z19 } from "zod";
|
|
11900
12653
|
|
|
11901
12654
|
// src/core/write/tagRename.ts
|
|
11902
|
-
import * as
|
|
11903
|
-
import * as
|
|
12655
|
+
import * as fs26 from "fs/promises";
|
|
12656
|
+
import * as path25 from "path";
|
|
11904
12657
|
import matter8 from "gray-matter";
|
|
11905
12658
|
import { getProtectedZones } from "@velvetmonkey/vault-core";
|
|
11906
12659
|
function getNotesInFolder3(index, folder) {
|
|
@@ -12006,10 +12759,10 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
|
|
|
12006
12759
|
const previews = [];
|
|
12007
12760
|
let totalChanges = 0;
|
|
12008
12761
|
for (const note of affectedNotes) {
|
|
12009
|
-
const fullPath =
|
|
12762
|
+
const fullPath = path25.join(vaultPath2, note.path);
|
|
12010
12763
|
let fileContent;
|
|
12011
12764
|
try {
|
|
12012
|
-
fileContent = await
|
|
12765
|
+
fileContent = await fs26.readFile(fullPath, "utf-8");
|
|
12013
12766
|
} catch {
|
|
12014
12767
|
continue;
|
|
12015
12768
|
}
|
|
@@ -12069,370 +12822,987 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
|
|
|
12069
12822
|
fm.tags = newTags;
|
|
12070
12823
|
}
|
|
12071
12824
|
}
|
|
12072
|
-
const { content: updatedContent, changes: contentChanges } = replaceInlineTags(
|
|
12073
|
-
parsed.content,
|
|
12074
|
-
cleanOld,
|
|
12075
|
-
cleanNew,
|
|
12076
|
-
renameChildren
|
|
12077
|
-
);
|
|
12078
|
-
preview.content_changes = contentChanges;
|
|
12079
|
-
preview.total_changes = preview.frontmatter_changes.length + preview.content_changes.length;
|
|
12080
|
-
totalChanges += preview.total_changes;
|
|
12081
|
-
if (preview.total_changes > 0) {
|
|
12082
|
-
previews.push(preview);
|
|
12083
|
-
if (!dryRun) {
|
|
12084
|
-
const newContent = matter8.stringify(updatedContent, fm);
|
|
12085
|
-
await
|
|
12825
|
+
const { content: updatedContent, changes: contentChanges } = replaceInlineTags(
|
|
12826
|
+
parsed.content,
|
|
12827
|
+
cleanOld,
|
|
12828
|
+
cleanNew,
|
|
12829
|
+
renameChildren
|
|
12830
|
+
);
|
|
12831
|
+
preview.content_changes = contentChanges;
|
|
12832
|
+
preview.total_changes = preview.frontmatter_changes.length + preview.content_changes.length;
|
|
12833
|
+
totalChanges += preview.total_changes;
|
|
12834
|
+
if (preview.total_changes > 0) {
|
|
12835
|
+
previews.push(preview);
|
|
12836
|
+
if (!dryRun) {
|
|
12837
|
+
const newContent = matter8.stringify(updatedContent, fm);
|
|
12838
|
+
await fs26.writeFile(fullPath, newContent, "utf-8");
|
|
12839
|
+
}
|
|
12840
|
+
}
|
|
12841
|
+
}
|
|
12842
|
+
return {
|
|
12843
|
+
old_tag: cleanOld,
|
|
12844
|
+
new_tag: cleanNew,
|
|
12845
|
+
rename_children: renameChildren,
|
|
12846
|
+
dry_run: dryRun,
|
|
12847
|
+
affected_notes: previews.length,
|
|
12848
|
+
total_changes: totalChanges,
|
|
12849
|
+
previews
|
|
12850
|
+
};
|
|
12851
|
+
}
|
|
12852
|
+
|
|
12853
|
+
// src/tools/write/tags.ts
|
|
12854
|
+
function registerTagTools(server2, getIndex, getVaultPath) {
|
|
12855
|
+
server2.registerTool(
|
|
12856
|
+
"rename_tag",
|
|
12857
|
+
{
|
|
12858
|
+
title: "Rename Tag",
|
|
12859
|
+
description: "Bulk rename a tag across all notes (frontmatter and inline). Supports hierarchical rename (#project \u2192 #work also transforms #project/active \u2192 #work/active). Dry-run by default (preview only). Handles deduplication when new tag already exists.",
|
|
12860
|
+
inputSchema: {
|
|
12861
|
+
old_tag: z19.string().describe('Tag to rename (without #, e.g., "project")'),
|
|
12862
|
+
new_tag: z19.string().describe('New tag name (without #, e.g., "work")'),
|
|
12863
|
+
rename_children: z19.boolean().optional().describe("Also rename child tags (e.g., #project/active \u2192 #work/active). Default: true"),
|
|
12864
|
+
folder: z19.string().optional().describe('Limit to notes in this folder (e.g., "projects")'),
|
|
12865
|
+
dry_run: z19.boolean().optional().describe("Preview only, no changes (default: true)"),
|
|
12866
|
+
commit: z19.boolean().optional().describe("Commit changes to git (default: false)")
|
|
12867
|
+
}
|
|
12868
|
+
},
|
|
12869
|
+
async ({ old_tag, new_tag, rename_children, folder, dry_run, commit }) => {
|
|
12870
|
+
const index = getIndex();
|
|
12871
|
+
const vaultPath2 = getVaultPath();
|
|
12872
|
+
const result = await renameTag(index, vaultPath2, old_tag, new_tag, {
|
|
12873
|
+
rename_children: rename_children ?? true,
|
|
12874
|
+
folder,
|
|
12875
|
+
dry_run: dry_run ?? true,
|
|
12876
|
+
commit: commit ?? false
|
|
12877
|
+
});
|
|
12878
|
+
return {
|
|
12879
|
+
content: [
|
|
12880
|
+
{
|
|
12881
|
+
type: "text",
|
|
12882
|
+
text: JSON.stringify(result, null, 2)
|
|
12883
|
+
}
|
|
12884
|
+
]
|
|
12885
|
+
};
|
|
12886
|
+
}
|
|
12887
|
+
);
|
|
12888
|
+
}
|
|
12889
|
+
|
|
12890
|
+
// src/tools/write/wikilinkFeedback.ts
|
|
12891
|
+
import { z as z20 } from "zod";
|
|
12892
|
+
function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
12893
|
+
server2.registerTool(
|
|
12894
|
+
"wikilink_feedback",
|
|
12895
|
+
{
|
|
12896
|
+
title: "Wikilink Feedback",
|
|
12897
|
+
description: 'Report and query wikilink accuracy feedback. Modes: "report" (record feedback), "list" (view recent feedback), "stats" (entity accuracy statistics). Entities with >=30% false positive rate (and >=10 samples) are auto-suppressed from future wikilink application.',
|
|
12898
|
+
inputSchema: {
|
|
12899
|
+
mode: z20.enum(["report", "list", "stats"]).describe("Operation mode"),
|
|
12900
|
+
entity: z20.string().optional().describe("Entity name (required for report mode, optional filter for list/stats)"),
|
|
12901
|
+
note_path: z20.string().optional().describe("Note path where the wikilink appeared (for report mode)"),
|
|
12902
|
+
context: z20.string().optional().describe("Surrounding text context (for report mode)"),
|
|
12903
|
+
correct: z20.boolean().optional().describe("Whether the wikilink was correct (for report mode)"),
|
|
12904
|
+
limit: z20.number().optional().describe("Max entries to return for list mode (default: 20)")
|
|
12905
|
+
}
|
|
12906
|
+
},
|
|
12907
|
+
async ({ mode, entity, note_path, context, correct, limit }) => {
|
|
12908
|
+
const stateDb2 = getStateDb();
|
|
12909
|
+
if (!stateDb2) {
|
|
12910
|
+
return {
|
|
12911
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
12912
|
+
};
|
|
12913
|
+
}
|
|
12914
|
+
let result;
|
|
12915
|
+
switch (mode) {
|
|
12916
|
+
case "report": {
|
|
12917
|
+
if (!entity || correct === void 0) {
|
|
12918
|
+
return {
|
|
12919
|
+
content: [{ type: "text", text: JSON.stringify({ error: "entity and correct are required for report mode" }) }]
|
|
12920
|
+
};
|
|
12921
|
+
}
|
|
12922
|
+
recordFeedback(stateDb2, entity, context || "", note_path || "", correct);
|
|
12923
|
+
const suppressionUpdated = updateSuppressionList(stateDb2) > 0;
|
|
12924
|
+
result = {
|
|
12925
|
+
mode: "report",
|
|
12926
|
+
reported: {
|
|
12927
|
+
entity,
|
|
12928
|
+
correct,
|
|
12929
|
+
suppression_updated: suppressionUpdated
|
|
12930
|
+
},
|
|
12931
|
+
total_suppressed: getSuppressedCount(stateDb2)
|
|
12932
|
+
};
|
|
12933
|
+
break;
|
|
12934
|
+
}
|
|
12935
|
+
case "list": {
|
|
12936
|
+
const entries = getFeedback(stateDb2, entity, limit ?? 20);
|
|
12937
|
+
result = {
|
|
12938
|
+
mode: "list",
|
|
12939
|
+
entries,
|
|
12940
|
+
total_feedback: entries.length
|
|
12941
|
+
};
|
|
12942
|
+
break;
|
|
12943
|
+
}
|
|
12944
|
+
case "stats": {
|
|
12945
|
+
const stats = getEntityStats(stateDb2);
|
|
12946
|
+
result = {
|
|
12947
|
+
mode: "stats",
|
|
12948
|
+
stats,
|
|
12949
|
+
total_feedback: stats.reduce((sum, s) => sum + s.total, 0),
|
|
12950
|
+
total_suppressed: getSuppressedCount(stateDb2)
|
|
12951
|
+
};
|
|
12952
|
+
break;
|
|
12953
|
+
}
|
|
12954
|
+
}
|
|
12955
|
+
return {
|
|
12956
|
+
content: [
|
|
12957
|
+
{
|
|
12958
|
+
type: "text",
|
|
12959
|
+
text: JSON.stringify(result, null, 2)
|
|
12960
|
+
}
|
|
12961
|
+
]
|
|
12962
|
+
};
|
|
12963
|
+
}
|
|
12964
|
+
);
|
|
12965
|
+
}
|
|
12966
|
+
|
|
12967
|
+
// src/tools/read/metrics.ts
|
|
12968
|
+
import { z as z21 } from "zod";
|
|
12969
|
+
|
|
12970
|
+
// src/core/shared/metrics.ts
|
|
12971
|
+
var ALL_METRICS = [
|
|
12972
|
+
"note_count",
|
|
12973
|
+
"link_count",
|
|
12974
|
+
"orphan_count",
|
|
12975
|
+
"tag_count",
|
|
12976
|
+
"entity_count",
|
|
12977
|
+
"avg_links_per_note",
|
|
12978
|
+
"link_density",
|
|
12979
|
+
"connected_ratio",
|
|
12980
|
+
"wikilink_accuracy",
|
|
12981
|
+
"wikilink_feedback_volume",
|
|
12982
|
+
"wikilink_suppressed_count"
|
|
12983
|
+
];
|
|
12984
|
+
function computeMetrics(index, stateDb2) {
|
|
12985
|
+
const noteCount = index.notes.size;
|
|
12986
|
+
let linkCount = 0;
|
|
12987
|
+
for (const note of index.notes.values()) {
|
|
12988
|
+
linkCount += note.outlinks.length;
|
|
12989
|
+
}
|
|
12990
|
+
const connectedNotes = /* @__PURE__ */ new Set();
|
|
12991
|
+
for (const [notePath, note] of index.notes) {
|
|
12992
|
+
if (note.outlinks.length > 0) {
|
|
12993
|
+
connectedNotes.add(notePath);
|
|
12994
|
+
}
|
|
12995
|
+
}
|
|
12996
|
+
for (const [target, backlinks] of index.backlinks) {
|
|
12997
|
+
for (const bl of backlinks) {
|
|
12998
|
+
connectedNotes.add(bl.source);
|
|
12999
|
+
}
|
|
13000
|
+
for (const note of index.notes.values()) {
|
|
13001
|
+
const normalizedTitle = note.title.toLowerCase();
|
|
13002
|
+
if (normalizedTitle === target.toLowerCase() || note.path.toLowerCase() === target.toLowerCase()) {
|
|
13003
|
+
connectedNotes.add(note.path);
|
|
12086
13004
|
}
|
|
12087
13005
|
}
|
|
12088
13006
|
}
|
|
13007
|
+
let orphanCount = 0;
|
|
13008
|
+
for (const [notePath, note] of index.notes) {
|
|
13009
|
+
if (!connectedNotes.has(notePath)) {
|
|
13010
|
+
orphanCount++;
|
|
13011
|
+
}
|
|
13012
|
+
}
|
|
13013
|
+
const tagCount = index.tags.size;
|
|
13014
|
+
const entityCount = index.entities.size;
|
|
13015
|
+
const avgLinksPerNote = noteCount > 0 ? linkCount / noteCount : 0;
|
|
13016
|
+
const possibleLinks = noteCount * (noteCount - 1);
|
|
13017
|
+
const linkDensity = possibleLinks > 0 ? linkCount / possibleLinks : 0;
|
|
13018
|
+
const connectedRatio = noteCount > 0 ? connectedNotes.size / noteCount : 0;
|
|
13019
|
+
let wikilinkAccuracy = 0;
|
|
13020
|
+
let wikilinkFeedbackVolume = 0;
|
|
13021
|
+
let wikilinkSuppressedCount = 0;
|
|
13022
|
+
if (stateDb2) {
|
|
13023
|
+
const entityStatsList = getEntityStats(stateDb2);
|
|
13024
|
+
wikilinkFeedbackVolume = entityStatsList.reduce((sum, s) => sum + s.total, 0);
|
|
13025
|
+
if (wikilinkFeedbackVolume > 0) {
|
|
13026
|
+
const totalCorrect = entityStatsList.reduce((sum, s) => sum + s.correct, 0);
|
|
13027
|
+
wikilinkAccuracy = Math.round(totalCorrect / wikilinkFeedbackVolume * 1e3) / 1e3;
|
|
13028
|
+
}
|
|
13029
|
+
wikilinkSuppressedCount = getSuppressedCount(stateDb2);
|
|
13030
|
+
}
|
|
12089
13031
|
return {
|
|
12090
|
-
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
13032
|
+
note_count: noteCount,
|
|
13033
|
+
link_count: linkCount,
|
|
13034
|
+
orphan_count: orphanCount,
|
|
13035
|
+
tag_count: tagCount,
|
|
13036
|
+
entity_count: entityCount,
|
|
13037
|
+
avg_links_per_note: Math.round(avgLinksPerNote * 100) / 100,
|
|
13038
|
+
link_density: Math.round(linkDensity * 1e4) / 1e4,
|
|
13039
|
+
connected_ratio: Math.round(connectedRatio * 1e3) / 1e3,
|
|
13040
|
+
wikilink_accuracy: wikilinkAccuracy,
|
|
13041
|
+
wikilink_feedback_volume: wikilinkFeedbackVolume,
|
|
13042
|
+
wikilink_suppressed_count: wikilinkSuppressedCount
|
|
12097
13043
|
};
|
|
12098
13044
|
}
|
|
13045
|
+
function recordMetrics(stateDb2, metrics) {
|
|
13046
|
+
const timestamp = Date.now();
|
|
13047
|
+
const insert = stateDb2.db.prepare(
|
|
13048
|
+
"INSERT INTO vault_metrics (timestamp, metric, value) VALUES (?, ?, ?)"
|
|
13049
|
+
);
|
|
13050
|
+
const transaction = stateDb2.db.transaction(() => {
|
|
13051
|
+
for (const [metric, value] of Object.entries(metrics)) {
|
|
13052
|
+
insert.run(timestamp, metric, value);
|
|
13053
|
+
}
|
|
13054
|
+
});
|
|
13055
|
+
transaction();
|
|
13056
|
+
}
|
|
13057
|
+
function getMetricHistory(stateDb2, metric, daysBack = 30) {
|
|
13058
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
13059
|
+
let rows;
|
|
13060
|
+
if (metric) {
|
|
13061
|
+
rows = stateDb2.db.prepare(
|
|
13062
|
+
"SELECT timestamp, metric, value FROM vault_metrics WHERE metric = ? AND timestamp >= ? ORDER BY timestamp"
|
|
13063
|
+
).all(metric, cutoff);
|
|
13064
|
+
} else {
|
|
13065
|
+
rows = stateDb2.db.prepare(
|
|
13066
|
+
"SELECT timestamp, metric, value FROM vault_metrics WHERE timestamp >= ? ORDER BY timestamp"
|
|
13067
|
+
).all(cutoff);
|
|
13068
|
+
}
|
|
13069
|
+
return rows.map((r) => ({
|
|
13070
|
+
metric: r.metric,
|
|
13071
|
+
value: r.value,
|
|
13072
|
+
timestamp: r.timestamp
|
|
13073
|
+
}));
|
|
13074
|
+
}
|
|
13075
|
+
function computeTrends(stateDb2, currentMetrics, daysBack = 30) {
|
|
13076
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
13077
|
+
const rows = stateDb2.db.prepare(`
|
|
13078
|
+
SELECT metric, value FROM vault_metrics
|
|
13079
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
13080
|
+
GROUP BY metric
|
|
13081
|
+
HAVING timestamp = MIN(timestamp)
|
|
13082
|
+
`).all(cutoff, cutoff + 24 * 60 * 60 * 1e3);
|
|
13083
|
+
const previousValues = /* @__PURE__ */ new Map();
|
|
13084
|
+
for (const row of rows) {
|
|
13085
|
+
previousValues.set(row.metric, row.value);
|
|
13086
|
+
}
|
|
13087
|
+
if (previousValues.size === 0) {
|
|
13088
|
+
const fallbackRows = stateDb2.db.prepare(`
|
|
13089
|
+
SELECT metric, MIN(value) as value FROM vault_metrics
|
|
13090
|
+
WHERE timestamp >= ?
|
|
13091
|
+
GROUP BY metric
|
|
13092
|
+
HAVING timestamp = MIN(timestamp)
|
|
13093
|
+
`).all(cutoff);
|
|
13094
|
+
for (const row of fallbackRows) {
|
|
13095
|
+
previousValues.set(row.metric, row.value);
|
|
13096
|
+
}
|
|
13097
|
+
}
|
|
13098
|
+
const trends = [];
|
|
13099
|
+
for (const metricName of ALL_METRICS) {
|
|
13100
|
+
const current = currentMetrics[metricName] ?? 0;
|
|
13101
|
+
const previous = previousValues.get(metricName) ?? current;
|
|
13102
|
+
const delta = current - previous;
|
|
13103
|
+
const deltaPct = previous !== 0 ? Math.round(delta / previous * 1e4) / 100 : delta !== 0 ? 100 : 0;
|
|
13104
|
+
let direction = "stable";
|
|
13105
|
+
if (delta > 0) direction = "up";
|
|
13106
|
+
if (delta < 0) direction = "down";
|
|
13107
|
+
trends.push({
|
|
13108
|
+
metric: metricName,
|
|
13109
|
+
current,
|
|
13110
|
+
previous,
|
|
13111
|
+
delta,
|
|
13112
|
+
delta_percent: deltaPct,
|
|
13113
|
+
direction
|
|
13114
|
+
});
|
|
13115
|
+
}
|
|
13116
|
+
return trends;
|
|
13117
|
+
}
|
|
13118
|
+
function purgeOldMetrics(stateDb2, retentionDays = 90) {
|
|
13119
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
13120
|
+
const result = stateDb2.db.prepare(
|
|
13121
|
+
"DELETE FROM vault_metrics WHERE timestamp < ?"
|
|
13122
|
+
).run(cutoff);
|
|
13123
|
+
return result.changes;
|
|
13124
|
+
}
|
|
12099
13125
|
|
|
12100
|
-
// src/tools/
|
|
12101
|
-
function
|
|
13126
|
+
// src/tools/read/metrics.ts
|
|
13127
|
+
function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
12102
13128
|
server2.registerTool(
|
|
12103
|
-
"
|
|
13129
|
+
"vault_growth",
|
|
12104
13130
|
{
|
|
12105
|
-
title: "
|
|
12106
|
-
description:
|
|
13131
|
+
title: "Vault Growth",
|
|
13132
|
+
description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago), "index_activity" (rebuild history). Tracks 11 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio, wikilink_accuracy, wikilink_feedback_volume, wikilink_suppressed_count.',
|
|
12107
13133
|
inputSchema: {
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
dry_run: z19.boolean().optional().describe("Preview only, no changes (default: true)"),
|
|
12113
|
-
commit: z19.boolean().optional().describe("Commit changes to git (default: false)")
|
|
13134
|
+
mode: z21.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
|
|
13135
|
+
metric: z21.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
|
|
13136
|
+
days_back: z21.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
|
|
13137
|
+
limit: z21.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
|
|
12114
13138
|
}
|
|
12115
13139
|
},
|
|
12116
|
-
async ({
|
|
13140
|
+
async ({ mode, metric, days_back, limit: eventLimit }) => {
|
|
12117
13141
|
const index = getIndex();
|
|
12118
|
-
const
|
|
12119
|
-
const
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
13142
|
+
const stateDb2 = getStateDb();
|
|
13143
|
+
const daysBack = days_back ?? 30;
|
|
13144
|
+
let result;
|
|
13145
|
+
switch (mode) {
|
|
13146
|
+
case "current": {
|
|
13147
|
+
const metrics = computeMetrics(index, stateDb2 ?? void 0);
|
|
13148
|
+
result = {
|
|
13149
|
+
mode: "current",
|
|
13150
|
+
metrics,
|
|
13151
|
+
recorded_at: Date.now()
|
|
13152
|
+
};
|
|
13153
|
+
break;
|
|
13154
|
+
}
|
|
13155
|
+
case "history": {
|
|
13156
|
+
if (!stateDb2) {
|
|
13157
|
+
return {
|
|
13158
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for historical queries" }) }]
|
|
13159
|
+
};
|
|
13160
|
+
}
|
|
13161
|
+
const history = getMetricHistory(stateDb2, metric, daysBack);
|
|
13162
|
+
result = {
|
|
13163
|
+
mode: "history",
|
|
13164
|
+
history
|
|
13165
|
+
};
|
|
13166
|
+
break;
|
|
13167
|
+
}
|
|
13168
|
+
case "trends": {
|
|
13169
|
+
if (!stateDb2) {
|
|
13170
|
+
return {
|
|
13171
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for trend analysis" }) }]
|
|
13172
|
+
};
|
|
13173
|
+
}
|
|
13174
|
+
const currentMetrics = computeMetrics(index, stateDb2);
|
|
13175
|
+
const trends = computeTrends(stateDb2, currentMetrics, daysBack);
|
|
13176
|
+
result = {
|
|
13177
|
+
mode: "trends",
|
|
13178
|
+
trends
|
|
13179
|
+
};
|
|
13180
|
+
break;
|
|
13181
|
+
}
|
|
13182
|
+
case "index_activity": {
|
|
13183
|
+
if (!stateDb2) {
|
|
13184
|
+
return {
|
|
13185
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for index activity queries" }) }]
|
|
13186
|
+
};
|
|
13187
|
+
}
|
|
13188
|
+
const summary = getIndexActivitySummary(stateDb2);
|
|
13189
|
+
const recentEvents = getRecentIndexEvents(stateDb2, eventLimit ?? 20);
|
|
13190
|
+
result = {
|
|
13191
|
+
mode: "index_activity",
|
|
13192
|
+
index_activity: { summary, recent_events: recentEvents }
|
|
13193
|
+
};
|
|
13194
|
+
break;
|
|
13195
|
+
}
|
|
13196
|
+
}
|
|
12125
13197
|
return {
|
|
12126
13198
|
content: [
|
|
12127
13199
|
{
|
|
12128
13200
|
type: "text",
|
|
12129
13201
|
text: JSON.stringify(result, null, 2)
|
|
12130
13202
|
}
|
|
12131
|
-
]
|
|
12132
|
-
};
|
|
13203
|
+
]
|
|
13204
|
+
};
|
|
13205
|
+
}
|
|
13206
|
+
);
|
|
13207
|
+
}
|
|
13208
|
+
|
|
13209
|
+
// src/tools/read/activity.ts
|
|
13210
|
+
import { z as z22 } from "zod";
|
|
13211
|
+
|
|
13212
|
+
// src/core/shared/toolTracking.ts
|
|
13213
|
+
function recordToolInvocation(stateDb2, event) {
|
|
13214
|
+
stateDb2.db.prepare(
|
|
13215
|
+
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
|
|
13216
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
13217
|
+
).run(
|
|
13218
|
+
Date.now(),
|
|
13219
|
+
event.tool_name,
|
|
13220
|
+
event.session_id ?? null,
|
|
13221
|
+
event.note_paths ? JSON.stringify(event.note_paths) : null,
|
|
13222
|
+
event.duration_ms ?? null,
|
|
13223
|
+
event.success !== false ? 1 : 0
|
|
13224
|
+
);
|
|
13225
|
+
}
|
|
13226
|
+
function rowToInvocation(row) {
|
|
13227
|
+
return {
|
|
13228
|
+
id: row.id,
|
|
13229
|
+
timestamp: row.timestamp,
|
|
13230
|
+
tool_name: row.tool_name,
|
|
13231
|
+
session_id: row.session_id,
|
|
13232
|
+
note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
|
|
13233
|
+
duration_ms: row.duration_ms,
|
|
13234
|
+
success: row.success === 1
|
|
13235
|
+
};
|
|
13236
|
+
}
|
|
13237
|
+
function getToolUsageSummary(stateDb2, daysBack = 30) {
|
|
13238
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
13239
|
+
const rows = stateDb2.db.prepare(`
|
|
13240
|
+
SELECT
|
|
13241
|
+
tool_name,
|
|
13242
|
+
COUNT(*) as invocation_count,
|
|
13243
|
+
AVG(duration_ms) as avg_duration_ms,
|
|
13244
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as success_rate,
|
|
13245
|
+
MAX(timestamp) as last_used
|
|
13246
|
+
FROM tool_invocations
|
|
13247
|
+
WHERE timestamp >= ?
|
|
13248
|
+
GROUP BY tool_name
|
|
13249
|
+
ORDER BY invocation_count DESC
|
|
13250
|
+
`).all(cutoff);
|
|
13251
|
+
return rows.map((r) => ({
|
|
13252
|
+
tool_name: r.tool_name,
|
|
13253
|
+
invocation_count: r.invocation_count,
|
|
13254
|
+
avg_duration_ms: Math.round(r.avg_duration_ms ?? 0),
|
|
13255
|
+
success_rate: Math.round(r.success_rate * 1e3) / 1e3,
|
|
13256
|
+
last_used: r.last_used
|
|
13257
|
+
}));
|
|
13258
|
+
}
|
|
13259
|
+
function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
13260
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
13261
|
+
const rows = stateDb2.db.prepare(`
|
|
13262
|
+
SELECT note_paths, tool_name, timestamp
|
|
13263
|
+
FROM tool_invocations
|
|
13264
|
+
WHERE timestamp >= ? AND note_paths IS NOT NULL
|
|
13265
|
+
ORDER BY timestamp DESC
|
|
13266
|
+
`).all(cutoff);
|
|
13267
|
+
const noteMap = /* @__PURE__ */ new Map();
|
|
13268
|
+
for (const row of rows) {
|
|
13269
|
+
let paths;
|
|
13270
|
+
try {
|
|
13271
|
+
paths = JSON.parse(row.note_paths);
|
|
13272
|
+
} catch {
|
|
13273
|
+
continue;
|
|
13274
|
+
}
|
|
13275
|
+
for (const p of paths) {
|
|
13276
|
+
const existing = noteMap.get(p);
|
|
13277
|
+
if (existing) {
|
|
13278
|
+
existing.access_count++;
|
|
13279
|
+
existing.last_accessed = Math.max(existing.last_accessed, row.timestamp);
|
|
13280
|
+
existing.tools.add(row.tool_name);
|
|
13281
|
+
} else {
|
|
13282
|
+
noteMap.set(p, {
|
|
13283
|
+
access_count: 1,
|
|
13284
|
+
last_accessed: row.timestamp,
|
|
13285
|
+
tools: /* @__PURE__ */ new Set([row.tool_name])
|
|
13286
|
+
});
|
|
13287
|
+
}
|
|
13288
|
+
}
|
|
13289
|
+
}
|
|
13290
|
+
return Array.from(noteMap.entries()).map(([path28, stats]) => ({
|
|
13291
|
+
path: path28,
|
|
13292
|
+
access_count: stats.access_count,
|
|
13293
|
+
last_accessed: stats.last_accessed,
|
|
13294
|
+
tools_used: Array.from(stats.tools)
|
|
13295
|
+
})).sort((a, b) => b.access_count - a.access_count);
|
|
13296
|
+
}
|
|
13297
|
+
function getSessionHistory(stateDb2, sessionId) {
|
|
13298
|
+
if (sessionId) {
|
|
13299
|
+
const rows2 = stateDb2.db.prepare(`
|
|
13300
|
+
SELECT * FROM tool_invocations
|
|
13301
|
+
WHERE session_id = ?
|
|
13302
|
+
ORDER BY timestamp
|
|
13303
|
+
`).all(sessionId);
|
|
13304
|
+
if (rows2.length === 0) return [];
|
|
13305
|
+
const tools = /* @__PURE__ */ new Set();
|
|
13306
|
+
const notes = /* @__PURE__ */ new Set();
|
|
13307
|
+
for (const row of rows2) {
|
|
13308
|
+
tools.add(row.tool_name);
|
|
13309
|
+
if (row.note_paths) {
|
|
13310
|
+
try {
|
|
13311
|
+
for (const p of JSON.parse(row.note_paths)) {
|
|
13312
|
+
notes.add(p);
|
|
13313
|
+
}
|
|
13314
|
+
} catch {
|
|
13315
|
+
}
|
|
13316
|
+
}
|
|
12133
13317
|
}
|
|
12134
|
-
|
|
13318
|
+
return [{
|
|
13319
|
+
session_id: sessionId,
|
|
13320
|
+
started_at: rows2[0].timestamp,
|
|
13321
|
+
last_activity: rows2[rows2.length - 1].timestamp,
|
|
13322
|
+
tool_count: rows2.length,
|
|
13323
|
+
unique_tools: Array.from(tools),
|
|
13324
|
+
notes_accessed: Array.from(notes)
|
|
13325
|
+
}];
|
|
13326
|
+
}
|
|
13327
|
+
const rows = stateDb2.db.prepare(`
|
|
13328
|
+
SELECT
|
|
13329
|
+
session_id,
|
|
13330
|
+
MIN(timestamp) as started_at,
|
|
13331
|
+
MAX(timestamp) as last_activity,
|
|
13332
|
+
COUNT(*) as tool_count
|
|
13333
|
+
FROM tool_invocations
|
|
13334
|
+
WHERE session_id IS NOT NULL
|
|
13335
|
+
GROUP BY session_id
|
|
13336
|
+
ORDER BY last_activity DESC
|
|
13337
|
+
LIMIT 20
|
|
13338
|
+
`).all();
|
|
13339
|
+
return rows.map((r) => ({
|
|
13340
|
+
session_id: r.session_id,
|
|
13341
|
+
started_at: r.started_at,
|
|
13342
|
+
last_activity: r.last_activity,
|
|
13343
|
+
tool_count: r.tool_count,
|
|
13344
|
+
unique_tools: [],
|
|
13345
|
+
notes_accessed: []
|
|
13346
|
+
}));
|
|
13347
|
+
}
|
|
13348
|
+
function getRecentInvocations(stateDb2, limit = 20) {
|
|
13349
|
+
const rows = stateDb2.db.prepare(
|
|
13350
|
+
"SELECT * FROM tool_invocations ORDER BY timestamp DESC LIMIT ?"
|
|
13351
|
+
).all(limit);
|
|
13352
|
+
return rows.map(rowToInvocation);
|
|
13353
|
+
}
|
|
13354
|
+
function purgeOldInvocations(stateDb2, retentionDays = 90) {
|
|
13355
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
13356
|
+
const result = stateDb2.db.prepare(
|
|
13357
|
+
"DELETE FROM tool_invocations WHERE timestamp < ?"
|
|
13358
|
+
).run(cutoff);
|
|
13359
|
+
return result.changes;
|
|
12135
13360
|
}
|
|
12136
13361
|
|
|
12137
|
-
// src/tools/
|
|
12138
|
-
|
|
12139
|
-
function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
13362
|
+
// src/tools/read/activity.ts
|
|
13363
|
+
function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
12140
13364
|
server2.registerTool(
|
|
12141
|
-
"
|
|
13365
|
+
"vault_activity",
|
|
12142
13366
|
{
|
|
12143
|
-
title: "
|
|
12144
|
-
description: '
|
|
13367
|
+
title: "Vault Activity",
|
|
13368
|
+
description: 'Track tool usage patterns and session activity. Modes:\n- "session": Current session summary (tools called, notes accessed)\n- "sessions": List of recent sessions\n- "note_access": Notes ranked by query frequency\n- "tool_usage": Tool usage patterns (most-used tools, avg duration)',
|
|
12145
13369
|
inputSchema: {
|
|
12146
|
-
mode:
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
correct: z20.boolean().optional().describe("Whether the wikilink was correct (for report mode)"),
|
|
12151
|
-
limit: z20.number().optional().describe("Max entries to return for list mode (default: 20)")
|
|
13370
|
+
mode: z22.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
|
|
13371
|
+
session_id: z22.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
|
|
13372
|
+
days_back: z22.number().optional().describe("Number of days to look back (default: 30)"),
|
|
13373
|
+
limit: z22.number().optional().describe("Maximum results to return (default: 20)")
|
|
12152
13374
|
}
|
|
12153
13375
|
},
|
|
12154
|
-
async ({ mode,
|
|
13376
|
+
async ({ mode, session_id, days_back, limit: resultLimit }) => {
|
|
12155
13377
|
const stateDb2 = getStateDb();
|
|
12156
13378
|
if (!stateDb2) {
|
|
12157
13379
|
return {
|
|
12158
13380
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
12159
13381
|
};
|
|
12160
13382
|
}
|
|
12161
|
-
|
|
13383
|
+
const daysBack = days_back ?? 30;
|
|
13384
|
+
const limit = resultLimit ?? 20;
|
|
12162
13385
|
switch (mode) {
|
|
12163
|
-
case "
|
|
12164
|
-
|
|
13386
|
+
case "session": {
|
|
13387
|
+
const sid = session_id ?? getSessionId2();
|
|
13388
|
+
if (!sid) {
|
|
12165
13389
|
return {
|
|
12166
|
-
content: [{ type: "text", text: JSON.stringify({ error: "
|
|
13390
|
+
content: [{ type: "text", text: JSON.stringify({ error: "No session ID available" }) }]
|
|
12167
13391
|
};
|
|
12168
13392
|
}
|
|
12169
|
-
|
|
12170
|
-
const
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
13393
|
+
const sessions = getSessionHistory(stateDb2, sid);
|
|
13394
|
+
const recent = getRecentInvocations(stateDb2, limit);
|
|
13395
|
+
const sessionInvocations = recent.filter((r) => r.session_id === sid);
|
|
13396
|
+
return {
|
|
13397
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
13398
|
+
mode: "session",
|
|
13399
|
+
session_id: sid,
|
|
13400
|
+
summary: sessions[0] ?? null,
|
|
13401
|
+
recent_invocations: sessionInvocations
|
|
13402
|
+
}, null, 2) }]
|
|
12179
13403
|
};
|
|
12180
|
-
break;
|
|
12181
13404
|
}
|
|
12182
|
-
case "
|
|
12183
|
-
const
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
13405
|
+
case "sessions": {
|
|
13406
|
+
const sessions = getSessionHistory(stateDb2);
|
|
13407
|
+
return {
|
|
13408
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
13409
|
+
mode: "sessions",
|
|
13410
|
+
sessions: sessions.slice(0, limit)
|
|
13411
|
+
}, null, 2) }]
|
|
12188
13412
|
};
|
|
12189
|
-
break;
|
|
12190
13413
|
}
|
|
12191
|
-
case "
|
|
12192
|
-
const
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
13414
|
+
case "note_access": {
|
|
13415
|
+
const notes = getNoteAccessFrequency(stateDb2, daysBack);
|
|
13416
|
+
return {
|
|
13417
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
13418
|
+
mode: "note_access",
|
|
13419
|
+
days_back: daysBack,
|
|
13420
|
+
notes: notes.slice(0, limit)
|
|
13421
|
+
}, null, 2) }]
|
|
13422
|
+
};
|
|
13423
|
+
}
|
|
13424
|
+
case "tool_usage": {
|
|
13425
|
+
const tools = getToolUsageSummary(stateDb2, daysBack);
|
|
13426
|
+
return {
|
|
13427
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
13428
|
+
mode: "tool_usage",
|
|
13429
|
+
days_back: daysBack,
|
|
13430
|
+
tools: tools.slice(0, limit)
|
|
13431
|
+
}, null, 2) }]
|
|
12198
13432
|
};
|
|
12199
|
-
break;
|
|
12200
13433
|
}
|
|
12201
13434
|
}
|
|
12202
|
-
return {
|
|
12203
|
-
content: [
|
|
12204
|
-
{
|
|
12205
|
-
type: "text",
|
|
12206
|
-
text: JSON.stringify(result, null, 2)
|
|
12207
|
-
}
|
|
12208
|
-
]
|
|
12209
|
-
};
|
|
12210
13435
|
}
|
|
12211
13436
|
);
|
|
12212
13437
|
}
|
|
12213
13438
|
|
|
12214
|
-
// src/tools/read/
|
|
12215
|
-
import { z as
|
|
13439
|
+
// src/tools/read/similarity.ts
|
|
13440
|
+
import { z as z23 } from "zod";
|
|
12216
13441
|
|
|
12217
|
-
// src/core/
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
"
|
|
12222
|
-
"
|
|
12223
|
-
"
|
|
12224
|
-
"
|
|
12225
|
-
"
|
|
12226
|
-
"
|
|
12227
|
-
"
|
|
12228
|
-
"
|
|
12229
|
-
"
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
13442
|
+
// src/core/read/similarity.ts
|
|
13443
|
+
import * as fs27 from "fs";
|
|
13444
|
+
import * as path26 from "path";
|
|
13445
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
13446
|
+
"the",
|
|
13447
|
+
"be",
|
|
13448
|
+
"to",
|
|
13449
|
+
"of",
|
|
13450
|
+
"and",
|
|
13451
|
+
"a",
|
|
13452
|
+
"in",
|
|
13453
|
+
"that",
|
|
13454
|
+
"have",
|
|
13455
|
+
"i",
|
|
13456
|
+
"it",
|
|
13457
|
+
"for",
|
|
13458
|
+
"not",
|
|
13459
|
+
"on",
|
|
13460
|
+
"with",
|
|
13461
|
+
"he",
|
|
13462
|
+
"as",
|
|
13463
|
+
"you",
|
|
13464
|
+
"do",
|
|
13465
|
+
"at",
|
|
13466
|
+
"this",
|
|
13467
|
+
"but",
|
|
13468
|
+
"his",
|
|
13469
|
+
"by",
|
|
13470
|
+
"from",
|
|
13471
|
+
"they",
|
|
13472
|
+
"we",
|
|
13473
|
+
"say",
|
|
13474
|
+
"her",
|
|
13475
|
+
"she",
|
|
13476
|
+
"or",
|
|
13477
|
+
"an",
|
|
13478
|
+
"will",
|
|
13479
|
+
"my",
|
|
13480
|
+
"one",
|
|
13481
|
+
"all",
|
|
13482
|
+
"would",
|
|
13483
|
+
"there",
|
|
13484
|
+
"their",
|
|
13485
|
+
"what",
|
|
13486
|
+
"so",
|
|
13487
|
+
"up",
|
|
13488
|
+
"out",
|
|
13489
|
+
"if",
|
|
13490
|
+
"about",
|
|
13491
|
+
"who",
|
|
13492
|
+
"get",
|
|
13493
|
+
"which",
|
|
13494
|
+
"go",
|
|
13495
|
+
"me",
|
|
13496
|
+
"when",
|
|
13497
|
+
"make",
|
|
13498
|
+
"can",
|
|
13499
|
+
"like",
|
|
13500
|
+
"time",
|
|
13501
|
+
"no",
|
|
13502
|
+
"just",
|
|
13503
|
+
"him",
|
|
13504
|
+
"know",
|
|
13505
|
+
"take",
|
|
13506
|
+
"people",
|
|
13507
|
+
"into",
|
|
13508
|
+
"year",
|
|
13509
|
+
"your",
|
|
13510
|
+
"good",
|
|
13511
|
+
"some",
|
|
13512
|
+
"could",
|
|
13513
|
+
"them",
|
|
13514
|
+
"see",
|
|
13515
|
+
"other",
|
|
13516
|
+
"than",
|
|
13517
|
+
"then",
|
|
13518
|
+
"now",
|
|
13519
|
+
"look",
|
|
13520
|
+
"only",
|
|
13521
|
+
"come",
|
|
13522
|
+
"its",
|
|
13523
|
+
"over",
|
|
13524
|
+
"think",
|
|
13525
|
+
"also",
|
|
13526
|
+
"back",
|
|
13527
|
+
"after",
|
|
13528
|
+
"use",
|
|
13529
|
+
"two",
|
|
13530
|
+
"how",
|
|
13531
|
+
"our",
|
|
13532
|
+
"work",
|
|
13533
|
+
"first",
|
|
13534
|
+
"well",
|
|
13535
|
+
"way",
|
|
13536
|
+
"even",
|
|
13537
|
+
"new",
|
|
13538
|
+
"want",
|
|
13539
|
+
"because",
|
|
13540
|
+
"any",
|
|
13541
|
+
"these",
|
|
13542
|
+
"give",
|
|
13543
|
+
"day",
|
|
13544
|
+
"most",
|
|
13545
|
+
"us",
|
|
13546
|
+
"is",
|
|
13547
|
+
"was",
|
|
13548
|
+
"are",
|
|
13549
|
+
"been",
|
|
13550
|
+
"has",
|
|
13551
|
+
"had",
|
|
13552
|
+
"did",
|
|
13553
|
+
"being",
|
|
13554
|
+
"were",
|
|
13555
|
+
"does",
|
|
13556
|
+
"done",
|
|
13557
|
+
"may",
|
|
13558
|
+
"should",
|
|
13559
|
+
"each",
|
|
13560
|
+
"much",
|
|
13561
|
+
"need",
|
|
13562
|
+
"very",
|
|
13563
|
+
"still",
|
|
13564
|
+
"between",
|
|
13565
|
+
"own"
|
|
13566
|
+
]);
|
|
13567
|
+
function extractKeyTerms(content, maxTerms = 15) {
|
|
13568
|
+
const bodyMatch = content.match(/^---[\s\S]*?---\n([\s\S]*)$/);
|
|
13569
|
+
const body = bodyMatch ? bodyMatch[1] : content;
|
|
13570
|
+
const cleaned = body.replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, "").replace(/\[\[([^\]|]+)(\|[^\]]+)?\]\]/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/https?:\/\/\S+/g, "").replace(/[#*_~>|=-]+/g, " ").replace(/\d+/g, " ");
|
|
13571
|
+
const words = cleaned.toLowerCase().split(/\W+/).filter((w) => w.length > 2);
|
|
13572
|
+
const freq = /* @__PURE__ */ new Map();
|
|
13573
|
+
for (const w of words) {
|
|
13574
|
+
if (STOP_WORDS.has(w)) continue;
|
|
13575
|
+
freq.set(w, (freq.get(w) || 0) + 1);
|
|
13576
|
+
}
|
|
13577
|
+
return Array.from(freq.entries()).sort((a, b) => b[1] - a[1]).slice(0, maxTerms).map(([word]) => word);
|
|
13578
|
+
}
|
|
13579
|
+
function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
|
|
13580
|
+
const limit = options.limit ?? 10;
|
|
13581
|
+
const absPath = path26.join(vaultPath2, sourcePath);
|
|
13582
|
+
let content;
|
|
13583
|
+
try {
|
|
13584
|
+
content = fs27.readFileSync(absPath, "utf-8");
|
|
13585
|
+
} catch {
|
|
13586
|
+
return [];
|
|
12277
13587
|
}
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
13588
|
+
const terms = extractKeyTerms(content);
|
|
13589
|
+
if (terms.length === 0) return [];
|
|
13590
|
+
const query = terms.join(" OR ");
|
|
13591
|
+
try {
|
|
13592
|
+
const results = db3.prepare(`
|
|
13593
|
+
SELECT
|
|
13594
|
+
path,
|
|
13595
|
+
title,
|
|
13596
|
+
bm25(notes_fts) as score,
|
|
13597
|
+
snippet(notes_fts, 2, '[', ']', '...', 15) as snippet
|
|
13598
|
+
FROM notes_fts
|
|
13599
|
+
WHERE notes_fts MATCH ?
|
|
13600
|
+
ORDER BY rank
|
|
13601
|
+
LIMIT ?
|
|
13602
|
+
`).all(query, limit + 20);
|
|
13603
|
+
let filtered = results.filter((r) => r.path !== sourcePath);
|
|
13604
|
+
if (options.excludeLinked) {
|
|
13605
|
+
const note = index.notes.get(sourcePath);
|
|
13606
|
+
if (note) {
|
|
13607
|
+
const linkedPaths = /* @__PURE__ */ new Set();
|
|
13608
|
+
for (const link of note.outlinks) {
|
|
13609
|
+
const resolved = index.entities.get(link.target.toLowerCase());
|
|
13610
|
+
if (resolved) linkedPaths.add(resolved);
|
|
13611
|
+
}
|
|
13612
|
+
const normalizedTitle = note.title.toLowerCase();
|
|
13613
|
+
const backlinks = index.backlinks.get(normalizedTitle) || [];
|
|
13614
|
+
for (const bl of backlinks) {
|
|
13615
|
+
linkedPaths.add(bl.source);
|
|
13616
|
+
}
|
|
13617
|
+
filtered = filtered.filter((r) => !linkedPaths.has(r.path));
|
|
13618
|
+
}
|
|
12300
13619
|
}
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
"SELECT timestamp, metric, value FROM vault_metrics WHERE metric = ? AND timestamp >= ? ORDER BY timestamp"
|
|
12310
|
-
).all(metric, cutoff);
|
|
12311
|
-
} else {
|
|
12312
|
-
rows = stateDb2.db.prepare(
|
|
12313
|
-
"SELECT timestamp, metric, value FROM vault_metrics WHERE timestamp >= ? ORDER BY timestamp"
|
|
12314
|
-
).all(cutoff);
|
|
13620
|
+
return filtered.slice(0, limit).map((r) => ({
|
|
13621
|
+
path: r.path,
|
|
13622
|
+
title: r.title,
|
|
13623
|
+
score: Math.round(Math.abs(r.score) * 1e3) / 1e3,
|
|
13624
|
+
snippet: r.snippet
|
|
13625
|
+
}));
|
|
13626
|
+
} catch {
|
|
13627
|
+
return [];
|
|
12315
13628
|
}
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
13629
|
+
}
|
|
13630
|
+
function getLinkedPaths(index, sourcePath) {
|
|
13631
|
+
const linkedPaths = /* @__PURE__ */ new Set();
|
|
13632
|
+
const note = index.notes.get(sourcePath);
|
|
13633
|
+
if (!note) return linkedPaths;
|
|
13634
|
+
for (const link of note.outlinks) {
|
|
13635
|
+
const resolved = index.entities.get(link.target.toLowerCase());
|
|
13636
|
+
if (resolved) linkedPaths.add(resolved);
|
|
13637
|
+
}
|
|
13638
|
+
const normalizedTitle = note.title.toLowerCase();
|
|
13639
|
+
const backlinks = index.backlinks.get(normalizedTitle) || [];
|
|
13640
|
+
for (const bl of backlinks) {
|
|
13641
|
+
linkedPaths.add(bl.source);
|
|
13642
|
+
}
|
|
13643
|
+
return linkedPaths;
|
|
13644
|
+
}
|
|
13645
|
+
async function findSemanticSimilarNotes(vaultPath2, index, sourcePath, options = {}) {
|
|
13646
|
+
const limit = options.limit ?? 10;
|
|
13647
|
+
if (!hasEmbeddingsIndex()) {
|
|
13648
|
+
await buildEmbeddingsIndex(vaultPath2);
|
|
13649
|
+
}
|
|
13650
|
+
const excludePaths = options.excludeLinked ? getLinkedPaths(index, sourcePath) : void 0;
|
|
13651
|
+
const results = await findSemanticallySimilar(sourcePath, limit, excludePaths);
|
|
13652
|
+
return results.map((r) => ({
|
|
13653
|
+
path: r.path,
|
|
13654
|
+
title: r.title,
|
|
13655
|
+
score: r.score,
|
|
13656
|
+
snippet: ""
|
|
13657
|
+
// Semantic results don't have snippets
|
|
12320
13658
|
}));
|
|
12321
13659
|
}
|
|
12322
|
-
function
|
|
12323
|
-
const
|
|
12324
|
-
const
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
}
|
|
12334
|
-
if (previousValues.size === 0) {
|
|
12335
|
-
const fallbackRows = stateDb2.db.prepare(`
|
|
12336
|
-
SELECT metric, MIN(value) as value FROM vault_metrics
|
|
12337
|
-
WHERE timestamp >= ?
|
|
12338
|
-
GROUP BY metric
|
|
12339
|
-
HAVING timestamp = MIN(timestamp)
|
|
12340
|
-
`).all(cutoff);
|
|
12341
|
-
for (const row of fallbackRows) {
|
|
12342
|
-
previousValues.set(row.metric, row.value);
|
|
12343
|
-
}
|
|
12344
|
-
}
|
|
12345
|
-
const trends = [];
|
|
12346
|
-
for (const metricName of ALL_METRICS) {
|
|
12347
|
-
const current = currentMetrics[metricName] ?? 0;
|
|
12348
|
-
const previous = previousValues.get(metricName) ?? current;
|
|
12349
|
-
const delta = current - previous;
|
|
12350
|
-
const deltaPct = previous !== 0 ? Math.round(delta / previous * 1e4) / 100 : delta !== 0 ? 100 : 0;
|
|
12351
|
-
let direction = "stable";
|
|
12352
|
-
if (delta > 0) direction = "up";
|
|
12353
|
-
if (delta < 0) direction = "down";
|
|
12354
|
-
trends.push({
|
|
12355
|
-
metric: metricName,
|
|
12356
|
-
current,
|
|
12357
|
-
previous,
|
|
12358
|
-
delta,
|
|
12359
|
-
delta_percent: deltaPct,
|
|
12360
|
-
direction
|
|
13660
|
+
async function findHybridSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
|
|
13661
|
+
const limit = options.limit ?? 10;
|
|
13662
|
+
const bm25Results = findSimilarNotes(db3, vaultPath2, index, sourcePath, {
|
|
13663
|
+
limit: limit * 2,
|
|
13664
|
+
excludeLinked: options.excludeLinked
|
|
13665
|
+
});
|
|
13666
|
+
let semanticResults;
|
|
13667
|
+
try {
|
|
13668
|
+
semanticResults = await findSemanticSimilarNotes(vaultPath2, index, sourcePath, {
|
|
13669
|
+
limit: limit * 2,
|
|
13670
|
+
excludeLinked: options.excludeLinked
|
|
12361
13671
|
});
|
|
13672
|
+
} catch {
|
|
13673
|
+
return bm25Results.slice(0, limit);
|
|
12362
13674
|
}
|
|
12363
|
-
|
|
12364
|
-
}
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
const
|
|
12368
|
-
|
|
12369
|
-
).
|
|
12370
|
-
|
|
13675
|
+
const rrfScores = reciprocalRankFusion(
|
|
13676
|
+
bm25Results.map((r) => ({ path: r.path })),
|
|
13677
|
+
semanticResults.map((r) => ({ path: r.path }))
|
|
13678
|
+
);
|
|
13679
|
+
const bm25Map = new Map(bm25Results.map((r) => [r.path, r]));
|
|
13680
|
+
const semanticMap = new Map(semanticResults.map((r) => [r.path, r]));
|
|
13681
|
+
const allPaths = /* @__PURE__ */ new Set([...bm25Results.map((r) => r.path), ...semanticResults.map((r) => r.path)]);
|
|
13682
|
+
const merged = Array.from(allPaths).map((p) => {
|
|
13683
|
+
const bm25 = bm25Map.get(p);
|
|
13684
|
+
const semantic = semanticMap.get(p);
|
|
13685
|
+
return {
|
|
13686
|
+
path: p,
|
|
13687
|
+
title: bm25?.title || semantic?.title || p.replace(/\.md$/, "").split("/").pop() || p,
|
|
13688
|
+
score: Math.round((rrfScores.get(p) || 0) * 1e4) / 1e4,
|
|
13689
|
+
snippet: bm25?.snippet || ""
|
|
13690
|
+
};
|
|
13691
|
+
});
|
|
13692
|
+
merged.sort((a, b) => b.score - a.score);
|
|
13693
|
+
return merged.slice(0, limit);
|
|
12371
13694
|
}
|
|
12372
13695
|
|
|
12373
|
-
// src/tools/read/
|
|
12374
|
-
function
|
|
13696
|
+
// src/tools/read/similarity.ts
|
|
13697
|
+
function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
12375
13698
|
server2.registerTool(
|
|
12376
|
-
"
|
|
13699
|
+
"find_similar",
|
|
12377
13700
|
{
|
|
12378
|
-
title: "
|
|
12379
|
-
description:
|
|
13701
|
+
title: "Find Similar Notes",
|
|
13702
|
+
description: "Find notes similar to a given note using FTS5 keyword matching. When embeddings have been built (via init_semantic), automatically uses hybrid ranking (BM25 + embedding similarity via Reciprocal Rank Fusion). Use exclude_linked to filter out notes already connected via wikilinks.",
|
|
12380
13703
|
inputSchema: {
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
13704
|
+
path: z23.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
|
|
13705
|
+
limit: z23.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
|
|
13706
|
+
exclude_linked: z23.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
|
|
12384
13707
|
}
|
|
12385
13708
|
},
|
|
12386
|
-
async ({
|
|
13709
|
+
async ({ path: path28, limit, exclude_linked }) => {
|
|
12387
13710
|
const index = getIndex();
|
|
13711
|
+
const vaultPath2 = getVaultPath();
|
|
12388
13712
|
const stateDb2 = getStateDb();
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
}
|
|
12401
|
-
case "history": {
|
|
12402
|
-
if (!stateDb2) {
|
|
12403
|
-
return {
|
|
12404
|
-
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for historical queries" }) }]
|
|
12405
|
-
};
|
|
12406
|
-
}
|
|
12407
|
-
const history = getMetricHistory(stateDb2, metric, daysBack);
|
|
12408
|
-
result = {
|
|
12409
|
-
mode: "history",
|
|
12410
|
-
history
|
|
12411
|
-
};
|
|
12412
|
-
break;
|
|
12413
|
-
}
|
|
12414
|
-
case "trends": {
|
|
12415
|
-
if (!stateDb2) {
|
|
12416
|
-
return {
|
|
12417
|
-
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for trend analysis" }) }]
|
|
12418
|
-
};
|
|
12419
|
-
}
|
|
12420
|
-
const currentMetrics = computeMetrics(index, stateDb2);
|
|
12421
|
-
const trends = computeTrends(stateDb2, currentMetrics, daysBack);
|
|
12422
|
-
result = {
|
|
12423
|
-
mode: "trends",
|
|
12424
|
-
trends
|
|
12425
|
-
};
|
|
12426
|
-
break;
|
|
12427
|
-
}
|
|
13713
|
+
if (!stateDb2) {
|
|
13714
|
+
return {
|
|
13715
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
13716
|
+
};
|
|
13717
|
+
}
|
|
13718
|
+
if (!index.notes.has(path28)) {
|
|
13719
|
+
return {
|
|
13720
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
13721
|
+
error: `Note not found: ${path28}`,
|
|
13722
|
+
hint: "Use the full relative path including .md extension"
|
|
13723
|
+
}, null, 2) }]
|
|
13724
|
+
};
|
|
12428
13725
|
}
|
|
13726
|
+
const opts = {
|
|
13727
|
+
limit: limit ?? 10,
|
|
13728
|
+
excludeLinked: exclude_linked ?? true
|
|
13729
|
+
};
|
|
13730
|
+
const useHybrid = hasEmbeddingsIndex();
|
|
13731
|
+
const method = useHybrid ? "hybrid" : "bm25";
|
|
13732
|
+
const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path28, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path28, opts);
|
|
12429
13733
|
return {
|
|
12430
|
-
content: [
|
|
12431
|
-
|
|
13734
|
+
content: [{
|
|
13735
|
+
type: "text",
|
|
13736
|
+
text: JSON.stringify({
|
|
13737
|
+
source: path28,
|
|
13738
|
+
method,
|
|
13739
|
+
exclude_linked: exclude_linked ?? true,
|
|
13740
|
+
count: results.length,
|
|
13741
|
+
similar: results
|
|
13742
|
+
}, null, 2)
|
|
13743
|
+
}]
|
|
13744
|
+
};
|
|
13745
|
+
}
|
|
13746
|
+
);
|
|
13747
|
+
}
|
|
13748
|
+
|
|
13749
|
+
// src/tools/read/semantic.ts
|
|
13750
|
+
import { z as z24 } from "zod";
|
|
13751
|
+
function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
13752
|
+
server2.registerTool(
|
|
13753
|
+
"init_semantic",
|
|
13754
|
+
{
|
|
13755
|
+
title: "Initialize Semantic Search",
|
|
13756
|
+
description: "Download the embedding model and build semantic search index for this vault. After running, search and find_similar automatically use hybrid ranking (BM25 + semantic). Run once per vault \u2014 subsequent calls skip already-embedded notes unless force=true.",
|
|
13757
|
+
inputSchema: {
|
|
13758
|
+
force: z24.boolean().optional().describe(
|
|
13759
|
+
"Rebuild all embeddings even if they already exist (default: false)"
|
|
13760
|
+
)
|
|
13761
|
+
}
|
|
13762
|
+
},
|
|
13763
|
+
async ({ force }) => {
|
|
13764
|
+
const stateDb2 = getStateDb();
|
|
13765
|
+
if (!stateDb2) {
|
|
13766
|
+
return {
|
|
13767
|
+
content: [{
|
|
12432
13768
|
type: "text",
|
|
12433
|
-
text: JSON.stringify(
|
|
12434
|
-
}
|
|
12435
|
-
|
|
13769
|
+
text: JSON.stringify({ error: "StateDb not available \u2014 vault may not be initialized yet" })
|
|
13770
|
+
}]
|
|
13771
|
+
};
|
|
13772
|
+
}
|
|
13773
|
+
setEmbeddingsDatabase(stateDb2.db);
|
|
13774
|
+
if (hasEmbeddingsIndex() && !force) {
|
|
13775
|
+
const count = getEmbeddingsCount();
|
|
13776
|
+
return {
|
|
13777
|
+
content: [{
|
|
13778
|
+
type: "text",
|
|
13779
|
+
text: JSON.stringify({
|
|
13780
|
+
success: true,
|
|
13781
|
+
already_built: true,
|
|
13782
|
+
embedded: count,
|
|
13783
|
+
hint: "Embeddings already built. All searches automatically use hybrid ranking."
|
|
13784
|
+
}, null, 2)
|
|
13785
|
+
}]
|
|
13786
|
+
};
|
|
13787
|
+
}
|
|
13788
|
+
const vaultPath2 = getVaultPath();
|
|
13789
|
+
const progress = await buildEmbeddingsIndex(vaultPath2, (p) => {
|
|
13790
|
+
if (p.current % 50 === 0 || p.current === p.total) {
|
|
13791
|
+
console.error(`[Semantic] Embedding ${p.current}/${p.total} notes (${p.skipped} skipped)...`);
|
|
13792
|
+
}
|
|
13793
|
+
});
|
|
13794
|
+
const embedded = progress.total - progress.skipped;
|
|
13795
|
+
return {
|
|
13796
|
+
content: [{
|
|
13797
|
+
type: "text",
|
|
13798
|
+
text: JSON.stringify({
|
|
13799
|
+
success: true,
|
|
13800
|
+
embedded,
|
|
13801
|
+
skipped: progress.skipped,
|
|
13802
|
+
total: progress.total,
|
|
13803
|
+
hint: "Embeddings built. All searches now automatically use hybrid ranking."
|
|
13804
|
+
}, null, 2)
|
|
13805
|
+
}]
|
|
12436
13806
|
};
|
|
12437
13807
|
}
|
|
12438
13808
|
);
|
|
@@ -12631,6 +14001,7 @@ var TOOL_CATEGORY = {
|
|
|
12631
14001
|
get_unlinked_mentions: "health",
|
|
12632
14002
|
// search (unified: metadata + content + entities)
|
|
12633
14003
|
search: "search",
|
|
14004
|
+
init_semantic: "search",
|
|
12634
14005
|
// backlinks
|
|
12635
14006
|
get_backlinks: "backlinks",
|
|
12636
14007
|
get_forward_links: "backlinks",
|
|
@@ -12677,7 +14048,11 @@ var TOOL_CATEGORY = {
|
|
|
12677
14048
|
// health (growth metrics)
|
|
12678
14049
|
vault_growth: "health",
|
|
12679
14050
|
// wikilinks (feedback)
|
|
12680
|
-
wikilink_feedback: "wikilinks"
|
|
14051
|
+
wikilink_feedback: "wikilinks",
|
|
14052
|
+
// health (activity tracking)
|
|
14053
|
+
vault_activity: "health",
|
|
14054
|
+
// schema (content similarity)
|
|
14055
|
+
find_similar: "schema"
|
|
12681
14056
|
};
|
|
12682
14057
|
var server = new McpServer({
|
|
12683
14058
|
name: "flywheel-memory",
|
|
@@ -12694,21 +14069,68 @@ function gateByCategory(name) {
|
|
|
12694
14069
|
_registeredCount++;
|
|
12695
14070
|
return true;
|
|
12696
14071
|
}
|
|
14072
|
+
function wrapHandlerWithTracking(toolName, handler) {
|
|
14073
|
+
return async (...args) => {
|
|
14074
|
+
const start = Date.now();
|
|
14075
|
+
let success = true;
|
|
14076
|
+
let notePaths;
|
|
14077
|
+
const params = args[0];
|
|
14078
|
+
if (params && typeof params === "object") {
|
|
14079
|
+
const paths = [];
|
|
14080
|
+
if (typeof params.path === "string") paths.push(params.path);
|
|
14081
|
+
if (Array.isArray(params.paths)) paths.push(...params.paths.filter((p) => typeof p === "string"));
|
|
14082
|
+
if (typeof params.note_path === "string") paths.push(params.note_path);
|
|
14083
|
+
if (typeof params.source === "string") paths.push(params.source);
|
|
14084
|
+
if (typeof params.target === "string") paths.push(params.target);
|
|
14085
|
+
if (paths.length > 0) notePaths = paths;
|
|
14086
|
+
}
|
|
14087
|
+
try {
|
|
14088
|
+
return await handler(...args);
|
|
14089
|
+
} catch (err) {
|
|
14090
|
+
success = false;
|
|
14091
|
+
throw err;
|
|
14092
|
+
} finally {
|
|
14093
|
+
if (stateDb) {
|
|
14094
|
+
try {
|
|
14095
|
+
let sessionId;
|
|
14096
|
+
try {
|
|
14097
|
+
sessionId = getSessionId();
|
|
14098
|
+
} catch {
|
|
14099
|
+
}
|
|
14100
|
+
recordToolInvocation(stateDb, {
|
|
14101
|
+
tool_name: toolName,
|
|
14102
|
+
session_id: sessionId,
|
|
14103
|
+
note_paths: notePaths,
|
|
14104
|
+
duration_ms: Date.now() - start,
|
|
14105
|
+
success
|
|
14106
|
+
});
|
|
14107
|
+
} catch {
|
|
14108
|
+
}
|
|
14109
|
+
}
|
|
14110
|
+
}
|
|
14111
|
+
};
|
|
14112
|
+
}
|
|
12697
14113
|
var _originalTool = server.tool.bind(server);
|
|
12698
14114
|
server.tool = (name, ...args) => {
|
|
12699
14115
|
if (!gateByCategory(name)) return;
|
|
14116
|
+
if (args.length > 0 && typeof args[args.length - 1] === "function") {
|
|
14117
|
+
args[args.length - 1] = wrapHandlerWithTracking(name, args[args.length - 1]);
|
|
14118
|
+
}
|
|
12700
14119
|
return _originalTool(name, ...args);
|
|
12701
14120
|
};
|
|
12702
14121
|
var _originalRegisterTool = server.registerTool?.bind(server);
|
|
12703
14122
|
if (_originalRegisterTool) {
|
|
12704
14123
|
server.registerTool = (name, ...args) => {
|
|
12705
14124
|
if (!gateByCategory(name)) return;
|
|
14125
|
+
if (args.length > 0 && typeof args[args.length - 1] === "function") {
|
|
14126
|
+
args[args.length - 1] = wrapHandlerWithTracking(name, args[args.length - 1]);
|
|
14127
|
+
}
|
|
12706
14128
|
return _originalRegisterTool(name, ...args);
|
|
12707
14129
|
};
|
|
12708
14130
|
}
|
|
12709
14131
|
var categoryList = Array.from(enabledCategories).sort().join(", ");
|
|
12710
14132
|
console.error(`[Memory] Tool categories: ${categoryList}`);
|
|
12711
|
-
registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
|
|
14133
|
+
registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
|
|
12712
14134
|
registerSystemTools(
|
|
12713
14135
|
server,
|
|
12714
14136
|
() => vaultIndex,
|
|
@@ -12725,7 +14147,7 @@ registerGraphTools(server, () => vaultIndex, () => vaultPath);
|
|
|
12725
14147
|
registerWikilinkTools(server, () => vaultIndex, () => vaultPath);
|
|
12726
14148
|
registerQueryTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
12727
14149
|
registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
|
|
12728
|
-
registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath);
|
|
14150
|
+
registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
12729
14151
|
registerVaultSchemaTools(server, () => vaultIndex, () => vaultPath);
|
|
12730
14152
|
registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath);
|
|
12731
14153
|
registerMigrationTools(server, () => vaultIndex, () => vaultPath);
|
|
@@ -12739,6 +14161,15 @@ registerPolicyTools(server, vaultPath);
|
|
|
12739
14161
|
registerTagTools(server, () => vaultIndex, () => vaultPath);
|
|
12740
14162
|
registerWikilinkFeedbackTools(server, () => stateDb);
|
|
12741
14163
|
registerMetricsTools(server, () => vaultIndex, () => stateDb);
|
|
14164
|
+
registerActivityTools(server, () => stateDb, () => {
|
|
14165
|
+
try {
|
|
14166
|
+
return getSessionId();
|
|
14167
|
+
} catch {
|
|
14168
|
+
return null;
|
|
14169
|
+
}
|
|
14170
|
+
});
|
|
14171
|
+
registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
14172
|
+
registerSemanticTools(server, () => vaultPath, () => stateDb);
|
|
12742
14173
|
registerVaultResources(server, () => vaultIndex ?? null);
|
|
12743
14174
|
console.error(`[Memory] Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
12744
14175
|
async function main() {
|
|
@@ -12749,6 +14180,7 @@ async function main() {
|
|
|
12749
14180
|
stateDb = openStateDb(vaultPath);
|
|
12750
14181
|
console.error("[Memory] StateDb initialized");
|
|
12751
14182
|
setFTS5Database(stateDb.db);
|
|
14183
|
+
setEmbeddingsDatabase(stateDb.db);
|
|
12752
14184
|
setWriteStateDb(stateDb);
|
|
12753
14185
|
await initializeEntityIndex(vaultPath);
|
|
12754
14186
|
} catch (err) {
|
|
@@ -12785,6 +14217,13 @@ async function main() {
|
|
|
12785
14217
|
setIndexState("ready");
|
|
12786
14218
|
const duration = Date.now() - startTime;
|
|
12787
14219
|
console.error(`[Memory] Index loaded from cache in ${duration}ms`);
|
|
14220
|
+
if (stateDb) {
|
|
14221
|
+
recordIndexEvent(stateDb, {
|
|
14222
|
+
trigger: "startup_cache",
|
|
14223
|
+
duration_ms: duration,
|
|
14224
|
+
note_count: cachedIndex.notes.size
|
|
14225
|
+
});
|
|
14226
|
+
}
|
|
12788
14227
|
runPostIndexWork(vaultIndex);
|
|
12789
14228
|
} else {
|
|
12790
14229
|
console.error("[Memory] Building vault index...");
|
|
@@ -12793,6 +14232,13 @@ async function main() {
|
|
|
12793
14232
|
setIndexState("ready");
|
|
12794
14233
|
const duration = Date.now() - startTime;
|
|
12795
14234
|
console.error(`[Memory] Vault index ready in ${duration}ms`);
|
|
14235
|
+
if (stateDb) {
|
|
14236
|
+
recordIndexEvent(stateDb, {
|
|
14237
|
+
trigger: "startup_build",
|
|
14238
|
+
duration_ms: duration,
|
|
14239
|
+
note_count: vaultIndex.notes.size
|
|
14240
|
+
});
|
|
14241
|
+
}
|
|
12796
14242
|
if (stateDb) {
|
|
12797
14243
|
try {
|
|
12798
14244
|
saveVaultIndexToCache(stateDb, vaultIndex);
|
|
@@ -12805,6 +14251,15 @@ async function main() {
|
|
|
12805
14251
|
} catch (err) {
|
|
12806
14252
|
setIndexState("error");
|
|
12807
14253
|
setIndexError(err instanceof Error ? err : new Error(String(err)));
|
|
14254
|
+
const duration = Date.now() - startTime;
|
|
14255
|
+
if (stateDb) {
|
|
14256
|
+
recordIndexEvent(stateDb, {
|
|
14257
|
+
trigger: "startup_build",
|
|
14258
|
+
duration_ms: duration,
|
|
14259
|
+
success: false,
|
|
14260
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14261
|
+
});
|
|
14262
|
+
}
|
|
12808
14263
|
console.error("[Memory] Failed to build vault index:", err);
|
|
12809
14264
|
}
|
|
12810
14265
|
}
|
|
@@ -12849,11 +14304,22 @@ async function runPostIndexWork(index) {
|
|
|
12849
14304
|
const metrics = computeMetrics(index, stateDb);
|
|
12850
14305
|
recordMetrics(stateDb, metrics);
|
|
12851
14306
|
purgeOldMetrics(stateDb, 90);
|
|
14307
|
+
purgeOldIndexEvents(stateDb, 90);
|
|
14308
|
+
purgeOldInvocations(stateDb, 90);
|
|
12852
14309
|
console.error("[Memory] Growth metrics recorded");
|
|
12853
14310
|
} catch (err) {
|
|
12854
14311
|
console.error("[Memory] Failed to record metrics:", err);
|
|
12855
14312
|
}
|
|
12856
14313
|
}
|
|
14314
|
+
if (stateDb) {
|
|
14315
|
+
try {
|
|
14316
|
+
const graphMetrics = computeGraphMetrics(index);
|
|
14317
|
+
recordGraphSnapshot(stateDb, graphMetrics);
|
|
14318
|
+
purgeOldSnapshots(stateDb, 90);
|
|
14319
|
+
} catch (err) {
|
|
14320
|
+
console.error("[Memory] Failed to record graph snapshot:", err);
|
|
14321
|
+
}
|
|
14322
|
+
}
|
|
12857
14323
|
if (stateDb) {
|
|
12858
14324
|
try {
|
|
12859
14325
|
updateSuppressionList(stateDb);
|
|
@@ -12878,13 +14344,37 @@ async function runPostIndexWork(index) {
|
|
|
12878
14344
|
config,
|
|
12879
14345
|
onBatch: async (batch) => {
|
|
12880
14346
|
console.error(`[Memory] Processing ${batch.events.length} file changes`);
|
|
12881
|
-
const
|
|
14347
|
+
const batchStart = Date.now();
|
|
14348
|
+
const changedPaths = batch.events.map((e) => e.path);
|
|
12882
14349
|
try {
|
|
12883
14350
|
vaultIndex = await buildVaultIndex(vaultPath);
|
|
12884
14351
|
setIndexState("ready");
|
|
12885
|
-
|
|
14352
|
+
const duration = Date.now() - batchStart;
|
|
14353
|
+
console.error(`[Memory] Index rebuilt in ${duration}ms`);
|
|
14354
|
+
if (stateDb) {
|
|
14355
|
+
recordIndexEvent(stateDb, {
|
|
14356
|
+
trigger: "watcher",
|
|
14357
|
+
duration_ms: duration,
|
|
14358
|
+
note_count: vaultIndex.notes.size,
|
|
14359
|
+
files_changed: batch.events.length,
|
|
14360
|
+
changed_paths: changedPaths
|
|
14361
|
+
});
|
|
14362
|
+
}
|
|
12886
14363
|
await updateEntitiesInStateDb();
|
|
12887
14364
|
await exportHubScores(vaultIndex, stateDb);
|
|
14365
|
+
if (hasEmbeddingsIndex()) {
|
|
14366
|
+
for (const event of batch.events) {
|
|
14367
|
+
try {
|
|
14368
|
+
if (event.type === "delete") {
|
|
14369
|
+
removeEmbedding(event.path);
|
|
14370
|
+
} else if (event.path.endsWith(".md")) {
|
|
14371
|
+
const absPath = path27.join(vaultPath, event.path);
|
|
14372
|
+
await updateEmbedding(event.path, absPath);
|
|
14373
|
+
}
|
|
14374
|
+
} catch {
|
|
14375
|
+
}
|
|
14376
|
+
}
|
|
14377
|
+
}
|
|
12888
14378
|
if (stateDb) {
|
|
12889
14379
|
try {
|
|
12890
14380
|
saveVaultIndexToCache(stateDb, vaultIndex);
|
|
@@ -12895,6 +14385,17 @@ async function runPostIndexWork(index) {
|
|
|
12895
14385
|
} catch (err) {
|
|
12896
14386
|
setIndexState("error");
|
|
12897
14387
|
setIndexError(err instanceof Error ? err : new Error(String(err)));
|
|
14388
|
+
const duration = Date.now() - batchStart;
|
|
14389
|
+
if (stateDb) {
|
|
14390
|
+
recordIndexEvent(stateDb, {
|
|
14391
|
+
trigger: "watcher",
|
|
14392
|
+
duration_ms: duration,
|
|
14393
|
+
success: false,
|
|
14394
|
+
files_changed: batch.events.length,
|
|
14395
|
+
changed_paths: changedPaths,
|
|
14396
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14397
|
+
});
|
|
14398
|
+
}
|
|
12898
14399
|
console.error("[Memory] Failed to rebuild index:", err);
|
|
12899
14400
|
}
|
|
12900
14401
|
},
|
|
@@ -12910,10 +14411,32 @@ async function runPostIndexWork(index) {
|
|
|
12910
14411
|
watcher.start();
|
|
12911
14412
|
}
|
|
12912
14413
|
}
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
});
|
|
14414
|
+
if (process.argv.includes("--init-semantic")) {
|
|
14415
|
+
(async () => {
|
|
14416
|
+
console.error("[Semantic] Pre-warming semantic search...");
|
|
14417
|
+
console.error(`[Semantic] Vault: ${vaultPath}`);
|
|
14418
|
+
try {
|
|
14419
|
+
const db3 = openStateDb(vaultPath);
|
|
14420
|
+
setEmbeddingsDatabase(db3.db);
|
|
14421
|
+
const progress = await buildEmbeddingsIndex(vaultPath, (p) => {
|
|
14422
|
+
if (p.current % 50 === 0 || p.current === p.total) {
|
|
14423
|
+
console.error(`[Semantic] Embedding ${p.current}/${p.total} notes (${p.skipped} skipped)...`);
|
|
14424
|
+
}
|
|
14425
|
+
});
|
|
14426
|
+
console.error(`[Semantic] Done. Embedded ${progress.total - progress.skipped} notes, skipped ${progress.skipped}.`);
|
|
14427
|
+
db3.close();
|
|
14428
|
+
process.exit(0);
|
|
14429
|
+
} catch (err) {
|
|
14430
|
+
console.error("[Semantic] Failed:", err instanceof Error ? err.message : err);
|
|
14431
|
+
process.exit(1);
|
|
14432
|
+
}
|
|
14433
|
+
})();
|
|
14434
|
+
} else {
|
|
14435
|
+
main().catch((error) => {
|
|
14436
|
+
console.error("[Memory] Fatal error:", error);
|
|
14437
|
+
process.exit(1);
|
|
14438
|
+
});
|
|
14439
|
+
}
|
|
12917
14440
|
process.on("beforeExit", async () => {
|
|
12918
14441
|
await flushLogs();
|
|
12919
14442
|
});
|