@velvetmonkey/flywheel-memory 2.0.18 → 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 +2338 -993
- 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
|
|
@@ -6316,7 +6534,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
6316
6534
|
const indexErrorObj = getIndexError();
|
|
6317
6535
|
let vaultAccessible = false;
|
|
6318
6536
|
try {
|
|
6319
|
-
|
|
6537
|
+
fs9.accessSync(vaultPath2, fs9.constants.R_OK);
|
|
6320
6538
|
vaultAccessible = true;
|
|
6321
6539
|
} catch {
|
|
6322
6540
|
vaultAccessible = false;
|
|
@@ -6447,8 +6665,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
6447
6665
|
daily_counts: z3.record(z3.number())
|
|
6448
6666
|
}).describe("Activity summary for the last 7 days")
|
|
6449
6667
|
};
|
|
6450
|
-
function isPeriodicNote(
|
|
6451
|
-
const filename =
|
|
6668
|
+
function isPeriodicNote(path28) {
|
|
6669
|
+
const filename = path28.split("/").pop() || "";
|
|
6452
6670
|
const nameWithoutExt = filename.replace(/\.md$/, "");
|
|
6453
6671
|
const patterns = [
|
|
6454
6672
|
/^\d{4}-\d{2}-\d{2}$/,
|
|
@@ -6463,7 +6681,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
6463
6681
|
// YYYY (yearly)
|
|
6464
6682
|
];
|
|
6465
6683
|
const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
|
|
6466
|
-
const folder =
|
|
6684
|
+
const folder = path28.split("/")[0]?.toLowerCase() || "";
|
|
6467
6685
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
6468
6686
|
}
|
|
6469
6687
|
server2.registerTool(
|
|
@@ -6632,10 +6850,10 @@ function sortNotes(notes, sortBy, order) {
|
|
|
6632
6850
|
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
6633
6851
|
server2.tool(
|
|
6634
6852
|
"search",
|
|
6635
|
-
'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" })',
|
|
6636
6854
|
{
|
|
6637
6855
|
query: z4.string().optional().describe('Search query text. Required for scope "content", "entities", "all". For "metadata" scope, use filters instead.'),
|
|
6638
|
-
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)."),
|
|
6639
6857
|
// Metadata filters (used with scope "metadata" or "all")
|
|
6640
6858
|
where: z4.record(z4.unknown()).optional().describe('Frontmatter filters as key-value pairs. Example: { "type": "project", "status": "active" }'),
|
|
6641
6859
|
has_tag: z4.string().optional().describe("Filter to notes with this tag"),
|
|
@@ -6744,12 +6962,42 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6744
6962
|
console.error("[FTS5] Index stale or missing, rebuilding...");
|
|
6745
6963
|
await buildFTS5Index(vaultPath2);
|
|
6746
6964
|
}
|
|
6747
|
-
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
|
+
}
|
|
6748
6995
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
6749
6996
|
scope: "content",
|
|
6997
|
+
method: "fts5",
|
|
6750
6998
|
query,
|
|
6751
|
-
total_results:
|
|
6752
|
-
results
|
|
6999
|
+
total_results: fts5Results.length,
|
|
7000
|
+
results: fts5Results
|
|
6753
7001
|
}, null, 2) }] };
|
|
6754
7002
|
}
|
|
6755
7003
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Invalid scope" }, null, 2) }] };
|
|
@@ -6758,7 +7006,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6758
7006
|
}
|
|
6759
7007
|
|
|
6760
7008
|
// src/tools/read/system.ts
|
|
6761
|
-
import * as
|
|
7009
|
+
import * as fs10 from "fs";
|
|
6762
7010
|
import * as path9 from "path";
|
|
6763
7011
|
import { z as z5 } from "zod";
|
|
6764
7012
|
import { scanVaultEntities as scanVaultEntities2 } from "@velvetmonkey/vault-core";
|
|
@@ -6993,7 +7241,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
6993
7241
|
}
|
|
6994
7242
|
try {
|
|
6995
7243
|
const fullPath = path9.join(vaultPath2, note.path);
|
|
6996
|
-
const content = await
|
|
7244
|
+
const content = await fs10.promises.readFile(fullPath, "utf-8");
|
|
6997
7245
|
const lines = content.split("\n");
|
|
6998
7246
|
for (let i = 0; i < lines.length; i++) {
|
|
6999
7247
|
const line = lines[i];
|
|
@@ -7109,7 +7357,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
7109
7357
|
if (include_word_count) {
|
|
7110
7358
|
try {
|
|
7111
7359
|
const fullPath = path9.join(vaultPath2, resolvedPath);
|
|
7112
|
-
const content = await
|
|
7360
|
+
const content = await fs10.promises.readFile(fullPath, "utf-8");
|
|
7113
7361
|
wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
7114
7362
|
} catch {
|
|
7115
7363
|
}
|
|
@@ -7211,7 +7459,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
7211
7459
|
import { z as z6 } from "zod";
|
|
7212
7460
|
|
|
7213
7461
|
// src/tools/read/structure.ts
|
|
7214
|
-
import * as
|
|
7462
|
+
import * as fs11 from "fs";
|
|
7215
7463
|
import * as path10 from "path";
|
|
7216
7464
|
var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
|
|
7217
7465
|
function extractHeadings(content) {
|
|
@@ -7269,7 +7517,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
|
|
|
7269
7517
|
const absolutePath = path10.join(vaultPath2, notePath);
|
|
7270
7518
|
let content;
|
|
7271
7519
|
try {
|
|
7272
|
-
content = await
|
|
7520
|
+
content = await fs11.promises.readFile(absolutePath, "utf-8");
|
|
7273
7521
|
} catch {
|
|
7274
7522
|
return null;
|
|
7275
7523
|
}
|
|
@@ -7292,7 +7540,7 @@ async function getSectionContent(index, notePath, headingText, vaultPath2, inclu
|
|
|
7292
7540
|
const absolutePath = path10.join(vaultPath2, notePath);
|
|
7293
7541
|
let content;
|
|
7294
7542
|
try {
|
|
7295
|
-
content = await
|
|
7543
|
+
content = await fs11.promises.readFile(absolutePath, "utf-8");
|
|
7296
7544
|
} catch {
|
|
7297
7545
|
return null;
|
|
7298
7546
|
}
|
|
@@ -7334,7 +7582,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
7334
7582
|
const absolutePath = path10.join(vaultPath2, note.path);
|
|
7335
7583
|
let content;
|
|
7336
7584
|
try {
|
|
7337
|
-
content = await
|
|
7585
|
+
content = await fs11.promises.readFile(absolutePath, "utf-8");
|
|
7338
7586
|
} catch {
|
|
7339
7587
|
continue;
|
|
7340
7588
|
}
|
|
@@ -7354,7 +7602,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
7354
7602
|
}
|
|
7355
7603
|
|
|
7356
7604
|
// src/tools/read/tasks.ts
|
|
7357
|
-
import * as
|
|
7605
|
+
import * as fs12 from "fs";
|
|
7358
7606
|
import * as path11 from "path";
|
|
7359
7607
|
var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
|
|
7360
7608
|
var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
|
|
@@ -7381,7 +7629,7 @@ function extractDueDate(text) {
|
|
|
7381
7629
|
async function extractTasksFromNote(notePath, absolutePath) {
|
|
7382
7630
|
let content;
|
|
7383
7631
|
try {
|
|
7384
|
-
content = await
|
|
7632
|
+
content = await fs12.promises.readFile(absolutePath, "utf-8");
|
|
7385
7633
|
} catch {
|
|
7386
7634
|
return [];
|
|
7387
7635
|
}
|
|
@@ -7486,18 +7734,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7486
7734
|
include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
|
|
7487
7735
|
}
|
|
7488
7736
|
},
|
|
7489
|
-
async ({ path:
|
|
7737
|
+
async ({ path: path28, include_content }) => {
|
|
7490
7738
|
const index = getIndex();
|
|
7491
7739
|
const vaultPath2 = getVaultPath();
|
|
7492
|
-
const result = await getNoteStructure(index,
|
|
7740
|
+
const result = await getNoteStructure(index, path28, vaultPath2);
|
|
7493
7741
|
if (!result) {
|
|
7494
7742
|
return {
|
|
7495
|
-
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) }]
|
|
7496
7744
|
};
|
|
7497
7745
|
}
|
|
7498
7746
|
if (include_content) {
|
|
7499
7747
|
for (const section of result.sections) {
|
|
7500
|
-
const sectionResult = await getSectionContent(index,
|
|
7748
|
+
const sectionResult = await getSectionContent(index, path28, section.heading.text, vaultPath2, true);
|
|
7501
7749
|
if (sectionResult) {
|
|
7502
7750
|
section.content = sectionResult.content;
|
|
7503
7751
|
}
|
|
@@ -7519,15 +7767,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7519
7767
|
include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
|
|
7520
7768
|
}
|
|
7521
7769
|
},
|
|
7522
|
-
async ({ path:
|
|
7770
|
+
async ({ path: path28, heading, include_subheadings }) => {
|
|
7523
7771
|
const index = getIndex();
|
|
7524
7772
|
const vaultPath2 = getVaultPath();
|
|
7525
|
-
const result = await getSectionContent(index,
|
|
7773
|
+
const result = await getSectionContent(index, path28, heading, vaultPath2, include_subheadings);
|
|
7526
7774
|
if (!result) {
|
|
7527
7775
|
return {
|
|
7528
7776
|
content: [{ type: "text", text: JSON.stringify({
|
|
7529
7777
|
error: "Section not found",
|
|
7530
|
-
path:
|
|
7778
|
+
path: path28,
|
|
7531
7779
|
heading
|
|
7532
7780
|
}, null, 2) }]
|
|
7533
7781
|
};
|
|
@@ -7581,16 +7829,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7581
7829
|
offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
7582
7830
|
}
|
|
7583
7831
|
},
|
|
7584
|
-
async ({ path:
|
|
7832
|
+
async ({ path: path28, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
|
|
7585
7833
|
const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
|
|
7586
7834
|
const index = getIndex();
|
|
7587
7835
|
const vaultPath2 = getVaultPath();
|
|
7588
7836
|
const config = getConfig();
|
|
7589
|
-
if (
|
|
7590
|
-
const result2 = await getTasksFromNote(index,
|
|
7837
|
+
if (path28) {
|
|
7838
|
+
const result2 = await getTasksFromNote(index, path28, vaultPath2, config.exclude_task_tags || []);
|
|
7591
7839
|
if (!result2) {
|
|
7592
7840
|
return {
|
|
7593
|
-
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) }]
|
|
7594
7842
|
};
|
|
7595
7843
|
}
|
|
7596
7844
|
let filtered = result2;
|
|
@@ -7600,7 +7848,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7600
7848
|
const paged2 = filtered.slice(offset, offset + limit);
|
|
7601
7849
|
return {
|
|
7602
7850
|
content: [{ type: "text", text: JSON.stringify({
|
|
7603
|
-
path:
|
|
7851
|
+
path: path28,
|
|
7604
7852
|
total_count: filtered.length,
|
|
7605
7853
|
returned_count: paged2.length,
|
|
7606
7854
|
open: result2.filter((t) => t.status === "open").length,
|
|
@@ -7715,7 +7963,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7715
7963
|
|
|
7716
7964
|
// src/tools/read/migrations.ts
|
|
7717
7965
|
import { z as z7 } from "zod";
|
|
7718
|
-
import * as
|
|
7966
|
+
import * as fs13 from "fs/promises";
|
|
7719
7967
|
import * as path12 from "path";
|
|
7720
7968
|
import matter2 from "gray-matter";
|
|
7721
7969
|
function getNotesInFolder(index, folder) {
|
|
@@ -7731,7 +7979,7 @@ function getNotesInFolder(index, folder) {
|
|
|
7731
7979
|
async function readFileContent(notePath, vaultPath2) {
|
|
7732
7980
|
const fullPath = path12.join(vaultPath2, notePath);
|
|
7733
7981
|
try {
|
|
7734
|
-
return await
|
|
7982
|
+
return await fs13.readFile(fullPath, "utf-8");
|
|
7735
7983
|
} catch {
|
|
7736
7984
|
return null;
|
|
7737
7985
|
}
|
|
@@ -7739,7 +7987,7 @@ async function readFileContent(notePath, vaultPath2) {
|
|
|
7739
7987
|
async function writeFileContent(notePath, vaultPath2, content) {
|
|
7740
7988
|
const fullPath = path12.join(vaultPath2, notePath);
|
|
7741
7989
|
try {
|
|
7742
|
-
await
|
|
7990
|
+
await fs13.writeFile(fullPath, content, "utf-8");
|
|
7743
7991
|
return true;
|
|
7744
7992
|
} catch {
|
|
7745
7993
|
return false;
|
|
@@ -7917,324 +8165,33 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
|
|
|
7917
8165
|
}
|
|
7918
8166
|
|
|
7919
8167
|
// src/tools/read/graphAnalysis.ts
|
|
8168
|
+
import fs14 from "node:fs";
|
|
8169
|
+
import path13 from "node:path";
|
|
7920
8170
|
import { z as z8 } from "zod";
|
|
7921
|
-
function registerGraphAnalysisTools(server2, getIndex, getVaultPath) {
|
|
7922
|
-
server2.registerTool(
|
|
7923
|
-
"graph_analysis",
|
|
7924
|
-
{
|
|
7925
|
-
title: "Graph Analysis",
|
|
7926
|
-
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 })',
|
|
7927
|
-
inputSchema: {
|
|
7928
|
-
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale"]).describe("Type of graph analysis to perform"),
|
|
7929
|
-
folder: z8.string().optional().describe("Limit to notes in this folder (orphans, dead_ends, sources)"),
|
|
7930
|
-
min_links: z8.coerce.number().default(5).describe("Minimum total connections for hubs"),
|
|
7931
|
-
min_backlinks: z8.coerce.number().default(1).describe("Minimum backlinks (dead_ends, stale)"),
|
|
7932
|
-
min_outlinks: z8.coerce.number().default(1).describe("Minimum outlinks (sources)"),
|
|
7933
|
-
days: z8.coerce.number().optional().describe("Notes not modified in this many days (stale, required)"),
|
|
7934
|
-
limit: z8.coerce.number().default(50).describe("Maximum number of results to return"),
|
|
7935
|
-
offset: z8.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
7936
|
-
}
|
|
7937
|
-
},
|
|
7938
|
-
async ({ analysis, folder, min_links, min_backlinks, min_outlinks, days, limit: requestedLimit, offset }) => {
|
|
7939
|
-
requireIndex();
|
|
7940
|
-
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
7941
|
-
const index = getIndex();
|
|
7942
|
-
switch (analysis) {
|
|
7943
|
-
case "orphans": {
|
|
7944
|
-
const allOrphans = findOrphanNotes(index, folder);
|
|
7945
|
-
const orphans = allOrphans.slice(offset, offset + limit);
|
|
7946
|
-
return {
|
|
7947
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7948
|
-
analysis: "orphans",
|
|
7949
|
-
orphan_count: allOrphans.length,
|
|
7950
|
-
returned_count: orphans.length,
|
|
7951
|
-
folder,
|
|
7952
|
-
orphans: orphans.map((o) => ({
|
|
7953
|
-
path: o.path,
|
|
7954
|
-
title: o.title,
|
|
7955
|
-
modified: o.modified.toISOString()
|
|
7956
|
-
}))
|
|
7957
|
-
}, null, 2) }]
|
|
7958
|
-
};
|
|
7959
|
-
}
|
|
7960
|
-
case "dead_ends": {
|
|
7961
|
-
const allResults = findDeadEnds(index, folder, min_backlinks);
|
|
7962
|
-
const result = allResults.slice(offset, offset + limit);
|
|
7963
|
-
return {
|
|
7964
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7965
|
-
analysis: "dead_ends",
|
|
7966
|
-
criteria: { folder, min_backlinks },
|
|
7967
|
-
total_count: allResults.length,
|
|
7968
|
-
returned_count: result.length,
|
|
7969
|
-
dead_ends: result
|
|
7970
|
-
}, null, 2) }]
|
|
7971
|
-
};
|
|
7972
|
-
}
|
|
7973
|
-
case "sources": {
|
|
7974
|
-
const allResults = findSources(index, folder, min_outlinks);
|
|
7975
|
-
const result = allResults.slice(offset, offset + limit);
|
|
7976
|
-
return {
|
|
7977
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7978
|
-
analysis: "sources",
|
|
7979
|
-
criteria: { folder, min_outlinks },
|
|
7980
|
-
total_count: allResults.length,
|
|
7981
|
-
returned_count: result.length,
|
|
7982
|
-
sources: result
|
|
7983
|
-
}, null, 2) }]
|
|
7984
|
-
};
|
|
7985
|
-
}
|
|
7986
|
-
case "hubs": {
|
|
7987
|
-
const allHubs = findHubNotes(index, min_links);
|
|
7988
|
-
const hubs = allHubs.slice(offset, offset + limit);
|
|
7989
|
-
return {
|
|
7990
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
7991
|
-
analysis: "hubs",
|
|
7992
|
-
hub_count: allHubs.length,
|
|
7993
|
-
returned_count: hubs.length,
|
|
7994
|
-
min_links,
|
|
7995
|
-
hubs: hubs.map((h) => ({
|
|
7996
|
-
path: h.path,
|
|
7997
|
-
title: h.title,
|
|
7998
|
-
backlink_count: h.backlink_count,
|
|
7999
|
-
forward_link_count: h.forward_link_count,
|
|
8000
|
-
total_connections: h.total_connections
|
|
8001
|
-
}))
|
|
8002
|
-
}, null, 2) }]
|
|
8003
|
-
};
|
|
8004
|
-
}
|
|
8005
|
-
case "stale": {
|
|
8006
|
-
if (days === void 0) {
|
|
8007
|
-
return {
|
|
8008
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
8009
|
-
error: "days parameter is required for stale analysis"
|
|
8010
|
-
}, null, 2) }]
|
|
8011
|
-
};
|
|
8012
|
-
}
|
|
8013
|
-
const result = getStaleNotes(index, days, min_backlinks).slice(0, limit);
|
|
8014
|
-
return {
|
|
8015
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
8016
|
-
analysis: "stale",
|
|
8017
|
-
criteria: { days, min_backlinks },
|
|
8018
|
-
count: result.length,
|
|
8019
|
-
notes: result.map((n) => ({
|
|
8020
|
-
...n,
|
|
8021
|
-
modified: n.modified.toISOString()
|
|
8022
|
-
}))
|
|
8023
|
-
}, null, 2) }]
|
|
8024
|
-
};
|
|
8025
|
-
}
|
|
8026
|
-
}
|
|
8027
|
-
}
|
|
8028
|
-
);
|
|
8029
|
-
}
|
|
8030
|
-
|
|
8031
|
-
// src/tools/read/vaultSchema.ts
|
|
8032
|
-
import { z as z9 } from "zod";
|
|
8033
8171
|
|
|
8034
|
-
// src/tools/read/
|
|
8172
|
+
// src/tools/read/schema.ts
|
|
8035
8173
|
function getValueType(value) {
|
|
8036
8174
|
if (value === null) return "null";
|
|
8037
8175
|
if (value === void 0) return "undefined";
|
|
8038
|
-
if (Array.isArray(value))
|
|
8176
|
+
if (Array.isArray(value)) {
|
|
8177
|
+
if (value.some((v) => typeof v === "string" && /^\[\[.+\]\]$/.test(v))) {
|
|
8178
|
+
return "wikilink[]";
|
|
8179
|
+
}
|
|
8180
|
+
return "array";
|
|
8181
|
+
}
|
|
8182
|
+
if (typeof value === "string") {
|
|
8183
|
+
if (/^\[\[.+\]\]$/.test(value)) return "wikilink";
|
|
8184
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) return "date";
|
|
8185
|
+
}
|
|
8039
8186
|
if (value instanceof Date) return "date";
|
|
8040
8187
|
return typeof value;
|
|
8041
8188
|
}
|
|
8042
|
-
function
|
|
8043
|
-
const
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
notesWithFrontmatter++;
|
|
8049
|
-
for (const [key, value] of Object.entries(fm)) {
|
|
8050
|
-
if (!fieldMap.has(key)) {
|
|
8051
|
-
fieldMap.set(key, {
|
|
8052
|
-
types: /* @__PURE__ */ new Set(),
|
|
8053
|
-
count: 0,
|
|
8054
|
-
examples: [],
|
|
8055
|
-
notes: []
|
|
8056
|
-
});
|
|
8057
|
-
}
|
|
8058
|
-
const info = fieldMap.get(key);
|
|
8059
|
-
info.count++;
|
|
8060
|
-
info.types.add(getValueType(value));
|
|
8061
|
-
if (info.examples.length < 5) {
|
|
8062
|
-
const valueStr = JSON.stringify(value);
|
|
8063
|
-
const existingStrs = info.examples.map((e) => JSON.stringify(e));
|
|
8064
|
-
if (!existingStrs.includes(valueStr)) {
|
|
8065
|
-
info.examples.push(value);
|
|
8066
|
-
}
|
|
8067
|
-
}
|
|
8068
|
-
if (info.notes.length < 5) {
|
|
8069
|
-
info.notes.push(note.path);
|
|
8070
|
-
}
|
|
8071
|
-
}
|
|
8072
|
-
}
|
|
8073
|
-
const fields = Array.from(fieldMap.entries()).map(([name, info]) => ({
|
|
8074
|
-
name,
|
|
8075
|
-
types: Array.from(info.types),
|
|
8076
|
-
count: info.count,
|
|
8077
|
-
examples: info.examples,
|
|
8078
|
-
notes_sample: info.notes
|
|
8079
|
-
})).sort((a, b) => b.count - a.count);
|
|
8080
|
-
return {
|
|
8081
|
-
total_notes: index.notes.size,
|
|
8082
|
-
notes_with_frontmatter: notesWithFrontmatter,
|
|
8083
|
-
field_count: fields.length,
|
|
8084
|
-
fields
|
|
8085
|
-
};
|
|
8086
|
-
}
|
|
8087
|
-
function getFieldValues(index, fieldName) {
|
|
8088
|
-
const valueMap = /* @__PURE__ */ new Map();
|
|
8089
|
-
let totalWithField = 0;
|
|
8090
|
-
for (const note of index.notes.values()) {
|
|
8091
|
-
const value = note.frontmatter[fieldName];
|
|
8092
|
-
if (value === void 0) continue;
|
|
8093
|
-
totalWithField++;
|
|
8094
|
-
const values = Array.isArray(value) ? value : [value];
|
|
8095
|
-
for (const v of values) {
|
|
8096
|
-
const key = JSON.stringify(v);
|
|
8097
|
-
if (!valueMap.has(key)) {
|
|
8098
|
-
valueMap.set(key, {
|
|
8099
|
-
value: v,
|
|
8100
|
-
count: 0,
|
|
8101
|
-
notes: []
|
|
8102
|
-
});
|
|
8103
|
-
}
|
|
8104
|
-
const info = valueMap.get(key);
|
|
8105
|
-
info.count++;
|
|
8106
|
-
info.notes.push(note.path);
|
|
8107
|
-
}
|
|
8108
|
-
}
|
|
8109
|
-
const valuesList = Array.from(valueMap.values()).sort((a, b) => b.count - a.count);
|
|
8110
|
-
return {
|
|
8111
|
-
field: fieldName,
|
|
8112
|
-
total_notes_with_field: totalWithField,
|
|
8113
|
-
unique_values: valuesList.length,
|
|
8114
|
-
values: valuesList
|
|
8115
|
-
};
|
|
8116
|
-
}
|
|
8117
|
-
function findFrontmatterInconsistencies(index) {
|
|
8118
|
-
const schema = getFrontmatterSchema(index);
|
|
8119
|
-
const inconsistencies = [];
|
|
8120
|
-
for (const field of schema.fields) {
|
|
8121
|
-
if (field.types.length > 1) {
|
|
8122
|
-
const examples = [];
|
|
8123
|
-
for (const note of index.notes.values()) {
|
|
8124
|
-
const value = note.frontmatter[field.name];
|
|
8125
|
-
if (value === void 0) continue;
|
|
8126
|
-
const type = getValueType(value);
|
|
8127
|
-
if (!examples.some((e) => e.type === type)) {
|
|
8128
|
-
examples.push({
|
|
8129
|
-
type,
|
|
8130
|
-
value,
|
|
8131
|
-
note: note.path
|
|
8132
|
-
});
|
|
8133
|
-
}
|
|
8134
|
-
if (examples.length >= field.types.length) break;
|
|
8135
|
-
}
|
|
8136
|
-
inconsistencies.push({
|
|
8137
|
-
field: field.name,
|
|
8138
|
-
types_found: field.types,
|
|
8139
|
-
examples
|
|
8140
|
-
});
|
|
8141
|
-
}
|
|
8142
|
-
}
|
|
8143
|
-
return inconsistencies;
|
|
8144
|
-
}
|
|
8145
|
-
function validateFrontmatter(index, schema, folder) {
|
|
8146
|
-
const results = [];
|
|
8147
|
-
for (const note of index.notes.values()) {
|
|
8148
|
-
if (folder && !note.path.startsWith(folder)) continue;
|
|
8149
|
-
const issues = [];
|
|
8150
|
-
for (const [fieldName, fieldSchema] of Object.entries(schema)) {
|
|
8151
|
-
const value = note.frontmatter[fieldName];
|
|
8152
|
-
if (fieldSchema.required && value === void 0) {
|
|
8153
|
-
issues.push({
|
|
8154
|
-
field: fieldName,
|
|
8155
|
-
issue: "missing",
|
|
8156
|
-
expected: "value required"
|
|
8157
|
-
});
|
|
8158
|
-
continue;
|
|
8159
|
-
}
|
|
8160
|
-
if (value === void 0) continue;
|
|
8161
|
-
if (fieldSchema.type) {
|
|
8162
|
-
const actualType = getValueType(value);
|
|
8163
|
-
const allowedTypes = Array.isArray(fieldSchema.type) ? fieldSchema.type : [fieldSchema.type];
|
|
8164
|
-
if (!allowedTypes.includes(actualType)) {
|
|
8165
|
-
issues.push({
|
|
8166
|
-
field: fieldName,
|
|
8167
|
-
issue: "wrong_type",
|
|
8168
|
-
expected: allowedTypes.join(" | "),
|
|
8169
|
-
actual: actualType
|
|
8170
|
-
});
|
|
8171
|
-
}
|
|
8172
|
-
}
|
|
8173
|
-
if (fieldSchema.values) {
|
|
8174
|
-
const valueStr = JSON.stringify(value);
|
|
8175
|
-
const allowedStrs = fieldSchema.values.map((v) => JSON.stringify(v));
|
|
8176
|
-
if (!allowedStrs.includes(valueStr)) {
|
|
8177
|
-
issues.push({
|
|
8178
|
-
field: fieldName,
|
|
8179
|
-
issue: "invalid_value",
|
|
8180
|
-
expected: fieldSchema.values.map((v) => String(v)).join(" | "),
|
|
8181
|
-
actual: String(value)
|
|
8182
|
-
});
|
|
8183
|
-
}
|
|
8184
|
-
}
|
|
8185
|
-
}
|
|
8186
|
-
if (issues.length > 0) {
|
|
8187
|
-
results.push({
|
|
8188
|
-
path: note.path,
|
|
8189
|
-
issues
|
|
8190
|
-
});
|
|
8191
|
-
}
|
|
8192
|
-
}
|
|
8193
|
-
return results;
|
|
8194
|
-
}
|
|
8195
|
-
function findMissingFrontmatter(index, folderSchemas) {
|
|
8196
|
-
const results = [];
|
|
8197
|
-
for (const note of index.notes.values()) {
|
|
8198
|
-
for (const [folder, requiredFields] of Object.entries(folderSchemas)) {
|
|
8199
|
-
if (!note.path.startsWith(folder)) continue;
|
|
8200
|
-
const missing = requiredFields.filter(
|
|
8201
|
-
(field) => note.frontmatter[field] === void 0
|
|
8202
|
-
);
|
|
8203
|
-
if (missing.length > 0) {
|
|
8204
|
-
results.push({
|
|
8205
|
-
path: note.path,
|
|
8206
|
-
folder,
|
|
8207
|
-
missing_fields: missing
|
|
8208
|
-
});
|
|
8209
|
-
}
|
|
8210
|
-
}
|
|
8211
|
-
}
|
|
8212
|
-
return results;
|
|
8213
|
-
}
|
|
8214
|
-
|
|
8215
|
-
// src/tools/read/schema.ts
|
|
8216
|
-
function getValueType2(value) {
|
|
8217
|
-
if (value === null) return "null";
|
|
8218
|
-
if (value === void 0) return "undefined";
|
|
8219
|
-
if (Array.isArray(value)) {
|
|
8220
|
-
if (value.some((v) => typeof v === "string" && /^\[\[.+\]\]$/.test(v))) {
|
|
8221
|
-
return "wikilink[]";
|
|
8222
|
-
}
|
|
8223
|
-
return "array";
|
|
8224
|
-
}
|
|
8225
|
-
if (typeof value === "string") {
|
|
8226
|
-
if (/^\[\[.+\]\]$/.test(value)) return "wikilink";
|
|
8227
|
-
if (/^\d{4}-\d{2}-\d{2}/.test(value)) return "date";
|
|
8228
|
-
}
|
|
8229
|
-
if (value instanceof Date) return "date";
|
|
8230
|
-
return typeof value;
|
|
8231
|
-
}
|
|
8232
|
-
function getFolder(notePath) {
|
|
8233
|
-
const lastSlash = notePath.lastIndexOf("/");
|
|
8234
|
-
return lastSlash === -1 ? "" : notePath.substring(0, lastSlash);
|
|
8235
|
-
}
|
|
8236
|
-
function getNotesInFolder2(index, folder) {
|
|
8237
|
-
const notes = [];
|
|
8189
|
+
function getFolder(notePath) {
|
|
8190
|
+
const lastSlash = notePath.lastIndexOf("/");
|
|
8191
|
+
return lastSlash === -1 ? "" : notePath.substring(0, lastSlash);
|
|
8192
|
+
}
|
|
8193
|
+
function getNotesInFolder2(index, folder) {
|
|
8194
|
+
const notes = [];
|
|
8238
8195
|
for (const note of index.notes.values()) {
|
|
8239
8196
|
if (!folder || note.path.startsWith(folder + "/") || getFolder(note.path) === folder) {
|
|
8240
8197
|
notes.push(note);
|
|
@@ -8298,7 +8255,7 @@ function inferFolderConventions(index, folder, minConfidence = 0.5) {
|
|
|
8298
8255
|
}
|
|
8299
8256
|
const stats = fieldStats.get(key);
|
|
8300
8257
|
stats.count++;
|
|
8301
|
-
const type =
|
|
8258
|
+
const type = getValueType(value);
|
|
8302
8259
|
stats.types.set(type, (stats.types.get(type) || 0) + 1);
|
|
8303
8260
|
const valueStr = JSON.stringify(value);
|
|
8304
8261
|
stats.values.set(valueStr, (stats.values.get(valueStr) || 0) + 1);
|
|
@@ -8430,7 +8387,7 @@ function suggestFieldValues(index, field, context) {
|
|
|
8430
8387
|
const value = note.frontmatter[field];
|
|
8431
8388
|
if (value === void 0) continue;
|
|
8432
8389
|
totalWithField++;
|
|
8433
|
-
const type =
|
|
8390
|
+
const type = getValueType(value);
|
|
8434
8391
|
typeCounts.set(type, (typeCounts.get(type) || 0) + 1);
|
|
8435
8392
|
const values = Array.isArray(value) ? value : [value];
|
|
8436
8393
|
for (const v of values) {
|
|
@@ -8489,148 +8446,480 @@ function suggestFieldValues(index, field, context) {
|
|
|
8489
8446
|
is_enumerable: isEnumerable
|
|
8490
8447
|
};
|
|
8491
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
|
+
}
|
|
8492
8520
|
|
|
8493
|
-
// src/
|
|
8494
|
-
function
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
{
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
})).optional().describe("Schema to validate against (validate mode)"),
|
|
8518
|
-
folder_schemas: z9.record(z9.array(z9.string())).optional().describe("Map of folder paths to required fields (missing mode)"),
|
|
8519
|
-
min_confidence: z9.coerce.number().min(0).max(1).optional().describe("Minimum confidence threshold (conventions)"),
|
|
8520
|
-
min_frequency: z9.coerce.number().min(0).max(1).optional().describe("Minimum field frequency (incomplete)"),
|
|
8521
|
-
existing_frontmatter: z9.record(z9.unknown()).optional().describe("Existing frontmatter for context (suggest_values)"),
|
|
8522
|
-
limit: z9.coerce.number().default(50).describe("Maximum results to return"),
|
|
8523
|
-
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);
|
|
8524
8545
|
}
|
|
8525
|
-
}
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
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);
|
|
8544
8583
|
}
|
|
8545
|
-
const result = getFieldValues(index, params.field);
|
|
8546
|
-
return {
|
|
8547
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
8548
|
-
};
|
|
8549
8584
|
}
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
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
|
+
}))
|
|
8759
|
+
}, null, 2) }]
|
|
8557
8760
|
};
|
|
8558
8761
|
}
|
|
8559
|
-
case "
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
8563
|
-
error: "schema parameter is required for validate analysis"
|
|
8564
|
-
}, null, 2) }]
|
|
8565
|
-
};
|
|
8566
|
-
}
|
|
8567
|
-
const result = validateFrontmatter(
|
|
8568
|
-
index,
|
|
8569
|
-
params.schema,
|
|
8570
|
-
params.folder
|
|
8571
|
-
);
|
|
8762
|
+
case "dead_ends": {
|
|
8763
|
+
const allResults = findDeadEnds(index, folder, min_backlinks);
|
|
8764
|
+
const result = allResults.slice(offset, offset + limit);
|
|
8572
8765
|
return {
|
|
8573
8766
|
content: [{ type: "text", text: JSON.stringify({
|
|
8574
|
-
|
|
8575
|
-
|
|
8767
|
+
analysis: "dead_ends",
|
|
8768
|
+
criteria: { folder, min_backlinks },
|
|
8769
|
+
total_count: allResults.length,
|
|
8770
|
+
returned_count: result.length,
|
|
8771
|
+
dead_ends: result
|
|
8576
8772
|
}, null, 2) }]
|
|
8577
8773
|
};
|
|
8578
8774
|
}
|
|
8579
|
-
case "
|
|
8580
|
-
|
|
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) {
|
|
8581
8809
|
return {
|
|
8582
8810
|
content: [{ type: "text", text: JSON.stringify({
|
|
8583
|
-
error: "
|
|
8811
|
+
error: "days parameter is required for stale analysis"
|
|
8584
8812
|
}, null, 2) }]
|
|
8585
8813
|
};
|
|
8586
8814
|
}
|
|
8587
|
-
const result =
|
|
8588
|
-
index,
|
|
8589
|
-
params.folder_schemas
|
|
8590
|
-
);
|
|
8815
|
+
const result = getStaleNotes(index, days, min_backlinks).slice(0, limit);
|
|
8591
8816
|
return {
|
|
8592
8817
|
content: [{ type: "text", text: JSON.stringify({
|
|
8593
|
-
|
|
8594
|
-
|
|
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
|
+
}))
|
|
8595
8825
|
}, null, 2) }]
|
|
8596
8826
|
};
|
|
8597
8827
|
}
|
|
8598
|
-
case "
|
|
8599
|
-
const
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
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
|
|
8603
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);
|
|
8604
8875
|
return {
|
|
8605
|
-
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) }]
|
|
8606
8884
|
};
|
|
8607
8885
|
}
|
|
8608
|
-
case "
|
|
8609
|
-
const
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
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);
|
|
8616
8897
|
return {
|
|
8617
|
-
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) }]
|
|
8618
8903
|
};
|
|
8619
8904
|
}
|
|
8620
|
-
case "
|
|
8621
|
-
|
|
8905
|
+
case "emerging_hubs": {
|
|
8906
|
+
const db3 = getStateDb?.();
|
|
8907
|
+
if (!db3) {
|
|
8622
8908
|
return {
|
|
8623
8909
|
content: [{ type: "text", text: JSON.stringify({
|
|
8624
|
-
error: "
|
|
8910
|
+
error: "StateDb not available \u2014 emerging hubs requires persistent state"
|
|
8625
8911
|
}, null, 2) }]
|
|
8626
8912
|
};
|
|
8627
8913
|
}
|
|
8628
|
-
const
|
|
8629
|
-
|
|
8630
|
-
existing_frontmatter: params.existing_frontmatter
|
|
8631
|
-
});
|
|
8914
|
+
const daysBack = days ?? 30;
|
|
8915
|
+
const hubs = getEmergingHubs(db3, daysBack);
|
|
8632
8916
|
return {
|
|
8633
|
-
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) }]
|
|
8634
8923
|
};
|
|
8635
8924
|
}
|
|
8636
8925
|
}
|
|
@@ -8638,45 +8927,392 @@ function registerVaultSchemaTools(server2, getIndex, getVaultPath) {
|
|
|
8638
8927
|
);
|
|
8639
8928
|
}
|
|
8640
8929
|
|
|
8641
|
-
// src/tools/read/
|
|
8642
|
-
import { z as
|
|
8930
|
+
// src/tools/read/vaultSchema.ts
|
|
8931
|
+
import { z as z9 } from "zod";
|
|
8643
8932
|
|
|
8644
|
-
// src/tools/read/
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
async function readFileContent2(notePath, vaultPath2) {
|
|
8652
|
-
const fullPath = path13.join(vaultPath2, notePath);
|
|
8653
|
-
try {
|
|
8654
|
-
return await fs13.readFile(fullPath, "utf-8");
|
|
8655
|
-
} catch {
|
|
8656
|
-
return null;
|
|
8657
|
-
}
|
|
8658
|
-
}
|
|
8659
|
-
function getBodyContent(content) {
|
|
8660
|
-
try {
|
|
8661
|
-
const parsed = matter3(content);
|
|
8662
|
-
const frontmatterMatch = content.match(/^---\n[\s\S]*?\n---\n?/);
|
|
8663
|
-
const bodyStartLine = frontmatterMatch ? frontmatterMatch[0].split("\n").length : 1;
|
|
8664
|
-
return { body: parsed.content, bodyStartLine };
|
|
8665
|
-
} catch {
|
|
8666
|
-
return { body: content, bodyStartLine: 1 };
|
|
8667
|
-
}
|
|
8668
|
-
}
|
|
8669
|
-
function removeCodeBlocks(content) {
|
|
8670
|
-
return content.replace(CODE_BLOCK_REGEX2, (match) => {
|
|
8671
|
-
const newlines = (match.match(/\n/g) || []).length;
|
|
8672
|
-
return "\n".repeat(newlines);
|
|
8673
|
-
});
|
|
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;
|
|
8674
8940
|
}
|
|
8675
|
-
function
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
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;
|
|
8680
9316
|
while ((match = WIKILINK_REGEX2.exec(value)) !== null) {
|
|
8681
9317
|
matches.push(match[1].trim());
|
|
8682
9318
|
}
|
|
@@ -8918,21 +9554,21 @@ async function validateCrossLayer(index, notePath, vaultPath2) {
|
|
|
8918
9554
|
}
|
|
8919
9555
|
|
|
8920
9556
|
// src/tools/read/computed.ts
|
|
8921
|
-
import * as
|
|
8922
|
-
import * as
|
|
9557
|
+
import * as fs16 from "fs/promises";
|
|
9558
|
+
import * as path15 from "path";
|
|
8923
9559
|
import matter4 from "gray-matter";
|
|
8924
9560
|
async function readFileContent3(notePath, vaultPath2) {
|
|
8925
|
-
const fullPath =
|
|
9561
|
+
const fullPath = path15.join(vaultPath2, notePath);
|
|
8926
9562
|
try {
|
|
8927
|
-
return await
|
|
9563
|
+
return await fs16.readFile(fullPath, "utf-8");
|
|
8928
9564
|
} catch {
|
|
8929
9565
|
return null;
|
|
8930
9566
|
}
|
|
8931
9567
|
}
|
|
8932
9568
|
async function getFileStats(notePath, vaultPath2) {
|
|
8933
|
-
const fullPath =
|
|
9569
|
+
const fullPath = path15.join(vaultPath2, notePath);
|
|
8934
9570
|
try {
|
|
8935
|
-
const stats = await
|
|
9571
|
+
const stats = await fs16.stat(fullPath);
|
|
8936
9572
|
return {
|
|
8937
9573
|
modified: stats.mtime,
|
|
8938
9574
|
created: stats.birthtime
|
|
@@ -9142,8 +9778,8 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
|
|
|
9142
9778
|
// src/tools/write/mutations.ts
|
|
9143
9779
|
init_writer();
|
|
9144
9780
|
import { z as z11 } from "zod";
|
|
9145
|
-
import
|
|
9146
|
-
import
|
|
9781
|
+
import fs19 from "fs/promises";
|
|
9782
|
+
import path18 from "path";
|
|
9147
9783
|
|
|
9148
9784
|
// src/core/write/validator.ts
|
|
9149
9785
|
var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
|
|
@@ -9345,8 +9981,8 @@ function runValidationPipeline(content, format, options = {}) {
|
|
|
9345
9981
|
|
|
9346
9982
|
// src/core/write/mutation-helpers.ts
|
|
9347
9983
|
init_writer();
|
|
9348
|
-
import
|
|
9349
|
-
import
|
|
9984
|
+
import fs18 from "fs/promises";
|
|
9985
|
+
import path17 from "path";
|
|
9350
9986
|
init_constants();
|
|
9351
9987
|
init_writer();
|
|
9352
9988
|
function formatMcpResult(result) {
|
|
@@ -9395,9 +10031,9 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
|
|
|
9395
10031
|
return info;
|
|
9396
10032
|
}
|
|
9397
10033
|
async function ensureFileExists(vaultPath2, notePath) {
|
|
9398
|
-
const fullPath =
|
|
10034
|
+
const fullPath = path17.join(vaultPath2, notePath);
|
|
9399
10035
|
try {
|
|
9400
|
-
await
|
|
10036
|
+
await fs18.access(fullPath);
|
|
9401
10037
|
return null;
|
|
9402
10038
|
} catch {
|
|
9403
10039
|
return errorResult(notePath, `File not found: ${notePath}`);
|
|
@@ -9495,10 +10131,10 @@ async function withVaultFrontmatter(options, operation) {
|
|
|
9495
10131
|
|
|
9496
10132
|
// src/tools/write/mutations.ts
|
|
9497
10133
|
async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
9498
|
-
const fullPath =
|
|
9499
|
-
await
|
|
10134
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
10135
|
+
await fs19.mkdir(path18.dirname(fullPath), { recursive: true });
|
|
9500
10136
|
const templates = config.templates || {};
|
|
9501
|
-
const filename =
|
|
10137
|
+
const filename = path18.basename(notePath, ".md").toLowerCase();
|
|
9502
10138
|
let templatePath;
|
|
9503
10139
|
const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
|
|
9504
10140
|
const weeklyPattern = /^\d{4}-W\d{2}/;
|
|
@@ -9519,10 +10155,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
9519
10155
|
let templateContent;
|
|
9520
10156
|
if (templatePath) {
|
|
9521
10157
|
try {
|
|
9522
|
-
const absTemplatePath =
|
|
9523
|
-
templateContent = await
|
|
10158
|
+
const absTemplatePath = path18.join(vaultPath2, templatePath);
|
|
10159
|
+
templateContent = await fs19.readFile(absTemplatePath, "utf-8");
|
|
9524
10160
|
} catch {
|
|
9525
|
-
const title =
|
|
10161
|
+
const title = path18.basename(notePath, ".md");
|
|
9526
10162
|
templateContent = `---
|
|
9527
10163
|
---
|
|
9528
10164
|
|
|
@@ -9531,7 +10167,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
9531
10167
|
templatePath = void 0;
|
|
9532
10168
|
}
|
|
9533
10169
|
} else {
|
|
9534
|
-
const title =
|
|
10170
|
+
const title = path18.basename(notePath, ".md");
|
|
9535
10171
|
templateContent = `---
|
|
9536
10172
|
---
|
|
9537
10173
|
|
|
@@ -9540,8 +10176,8 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
9540
10176
|
}
|
|
9541
10177
|
const now = /* @__PURE__ */ new Date();
|
|
9542
10178
|
const dateStr = now.toISOString().split("T")[0];
|
|
9543
|
-
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g,
|
|
9544
|
-
await
|
|
10179
|
+
templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path18.basename(notePath, ".md"));
|
|
10180
|
+
await fs19.writeFile(fullPath, templateContent, "utf-8");
|
|
9545
10181
|
return { created: true, templateUsed: templatePath };
|
|
9546
10182
|
}
|
|
9547
10183
|
function registerMutationTools(server2, vaultPath2, getConfig = () => ({})) {
|
|
@@ -9572,9 +10208,9 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
9572
10208
|
let noteCreated = false;
|
|
9573
10209
|
let templateUsed;
|
|
9574
10210
|
if (create_if_missing) {
|
|
9575
|
-
const fullPath =
|
|
10211
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
9576
10212
|
try {
|
|
9577
|
-
await
|
|
10213
|
+
await fs19.access(fullPath);
|
|
9578
10214
|
} catch {
|
|
9579
10215
|
const config = getConfig();
|
|
9580
10216
|
const result = await createNoteFromTemplate(vaultPath2, notePath, config);
|
|
@@ -10009,8 +10645,8 @@ Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { st
|
|
|
10009
10645
|
// src/tools/write/notes.ts
|
|
10010
10646
|
init_writer();
|
|
10011
10647
|
import { z as z14 } from "zod";
|
|
10012
|
-
import
|
|
10013
|
-
import
|
|
10648
|
+
import fs20 from "fs/promises";
|
|
10649
|
+
import path19 from "path";
|
|
10014
10650
|
function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
10015
10651
|
server2.tool(
|
|
10016
10652
|
"vault_create_note",
|
|
@@ -10033,23 +10669,23 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
10033
10669
|
if (!validatePath(vaultPath2, notePath)) {
|
|
10034
10670
|
return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
|
|
10035
10671
|
}
|
|
10036
|
-
const fullPath =
|
|
10672
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
10037
10673
|
const existsCheck = await ensureFileExists(vaultPath2, notePath);
|
|
10038
10674
|
if (existsCheck === null && !overwrite) {
|
|
10039
10675
|
return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
|
|
10040
10676
|
}
|
|
10041
|
-
const dir =
|
|
10042
|
-
await
|
|
10677
|
+
const dir = path19.dirname(fullPath);
|
|
10678
|
+
await fs20.mkdir(dir, { recursive: true });
|
|
10043
10679
|
let effectiveContent = content;
|
|
10044
10680
|
let effectiveFrontmatter = frontmatter;
|
|
10045
10681
|
if (template) {
|
|
10046
|
-
const templatePath =
|
|
10682
|
+
const templatePath = path19.join(vaultPath2, template);
|
|
10047
10683
|
try {
|
|
10048
|
-
const raw = await
|
|
10684
|
+
const raw = await fs20.readFile(templatePath, "utf-8");
|
|
10049
10685
|
const matter9 = (await import("gray-matter")).default;
|
|
10050
10686
|
const parsed = matter9(raw);
|
|
10051
10687
|
const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
10052
|
-
const title =
|
|
10688
|
+
const title = path19.basename(notePath, ".md");
|
|
10053
10689
|
let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
|
|
10054
10690
|
if (content) {
|
|
10055
10691
|
templateContent = templateContent.trimEnd() + "\n\n" + content;
|
|
@@ -10061,7 +10697,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
10061
10697
|
}
|
|
10062
10698
|
}
|
|
10063
10699
|
const warnings = [];
|
|
10064
|
-
const noteName =
|
|
10700
|
+
const noteName = path19.basename(notePath, ".md");
|
|
10065
10701
|
const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
|
|
10066
10702
|
const preflight = checkPreflightSimilarity(noteName);
|
|
10067
10703
|
if (preflight.existingEntity) {
|
|
@@ -10178,8 +10814,8 @@ ${sources}`;
|
|
|
10178
10814
|
}
|
|
10179
10815
|
return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
|
|
10180
10816
|
}
|
|
10181
|
-
const fullPath =
|
|
10182
|
-
await
|
|
10817
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
10818
|
+
await fs20.unlink(fullPath);
|
|
10183
10819
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
|
|
10184
10820
|
const message = backlinkWarning ? `Deleted note: ${notePath}
|
|
10185
10821
|
|
|
@@ -10197,8 +10833,8 @@ Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
|
|
|
10197
10833
|
// src/tools/write/move-notes.ts
|
|
10198
10834
|
init_writer();
|
|
10199
10835
|
import { z as z15 } from "zod";
|
|
10200
|
-
import
|
|
10201
|
-
import
|
|
10836
|
+
import fs21 from "fs/promises";
|
|
10837
|
+
import path20 from "path";
|
|
10202
10838
|
import matter6 from "gray-matter";
|
|
10203
10839
|
function escapeRegex(str) {
|
|
10204
10840
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -10217,16 +10853,16 @@ function extractWikilinks2(content) {
|
|
|
10217
10853
|
return wikilinks;
|
|
10218
10854
|
}
|
|
10219
10855
|
function getTitleFromPath(filePath) {
|
|
10220
|
-
return
|
|
10856
|
+
return path20.basename(filePath, ".md");
|
|
10221
10857
|
}
|
|
10222
10858
|
async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
10223
10859
|
const results = [];
|
|
10224
10860
|
const allTargets = [targetTitle, ...targetAliases].map((t) => t.toLowerCase());
|
|
10225
10861
|
async function scanDir(dir) {
|
|
10226
10862
|
const files = [];
|
|
10227
|
-
const entries = await
|
|
10863
|
+
const entries = await fs21.readdir(dir, { withFileTypes: true });
|
|
10228
10864
|
for (const entry of entries) {
|
|
10229
|
-
const fullPath =
|
|
10865
|
+
const fullPath = path20.join(dir, entry.name);
|
|
10230
10866
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
10231
10867
|
files.push(...await scanDir(fullPath));
|
|
10232
10868
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -10237,8 +10873,8 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
10237
10873
|
}
|
|
10238
10874
|
const allFiles = await scanDir(vaultPath2);
|
|
10239
10875
|
for (const filePath of allFiles) {
|
|
10240
|
-
const relativePath =
|
|
10241
|
-
const content = await
|
|
10876
|
+
const relativePath = path20.relative(vaultPath2, filePath);
|
|
10877
|
+
const content = await fs21.readFile(filePath, "utf-8");
|
|
10242
10878
|
const wikilinks = extractWikilinks2(content);
|
|
10243
10879
|
const matchingLinks = [];
|
|
10244
10880
|
for (const link of wikilinks) {
|
|
@@ -10257,8 +10893,8 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
10257
10893
|
return results;
|
|
10258
10894
|
}
|
|
10259
10895
|
async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
|
|
10260
|
-
const fullPath =
|
|
10261
|
-
const raw = await
|
|
10896
|
+
const fullPath = path20.join(vaultPath2, filePath);
|
|
10897
|
+
const raw = await fs21.readFile(fullPath, "utf-8");
|
|
10262
10898
|
const parsed = matter6(raw);
|
|
10263
10899
|
let content = parsed.content;
|
|
10264
10900
|
let totalUpdated = 0;
|
|
@@ -10324,10 +10960,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10324
10960
|
};
|
|
10325
10961
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10326
10962
|
}
|
|
10327
|
-
const oldFullPath =
|
|
10328
|
-
const newFullPath =
|
|
10963
|
+
const oldFullPath = path20.join(vaultPath2, oldPath);
|
|
10964
|
+
const newFullPath = path20.join(vaultPath2, newPath);
|
|
10329
10965
|
try {
|
|
10330
|
-
await
|
|
10966
|
+
await fs21.access(oldFullPath);
|
|
10331
10967
|
} catch {
|
|
10332
10968
|
const result2 = {
|
|
10333
10969
|
success: false,
|
|
@@ -10337,7 +10973,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10337
10973
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10338
10974
|
}
|
|
10339
10975
|
try {
|
|
10340
|
-
await
|
|
10976
|
+
await fs21.access(newFullPath);
|
|
10341
10977
|
const result2 = {
|
|
10342
10978
|
success: false,
|
|
10343
10979
|
message: `Destination already exists: ${newPath}`,
|
|
@@ -10346,7 +10982,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10346
10982
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10347
10983
|
} catch {
|
|
10348
10984
|
}
|
|
10349
|
-
const sourceContent = await
|
|
10985
|
+
const sourceContent = await fs21.readFile(oldFullPath, "utf-8");
|
|
10350
10986
|
const parsed = matter6(sourceContent);
|
|
10351
10987
|
const aliases = extractAliases2(parsed.data);
|
|
10352
10988
|
const oldTitle = getTitleFromPath(oldPath);
|
|
@@ -10375,9 +11011,9 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10375
11011
|
}
|
|
10376
11012
|
}
|
|
10377
11013
|
}
|
|
10378
|
-
const destDir =
|
|
10379
|
-
await
|
|
10380
|
-
await
|
|
11014
|
+
const destDir = path20.dirname(newFullPath);
|
|
11015
|
+
await fs21.mkdir(destDir, { recursive: true });
|
|
11016
|
+
await fs21.rename(oldFullPath, newFullPath);
|
|
10381
11017
|
let gitCommit;
|
|
10382
11018
|
let undoAvailable;
|
|
10383
11019
|
let staleLockDetected;
|
|
@@ -10461,12 +11097,12 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10461
11097
|
if (sanitizedTitle !== newTitle) {
|
|
10462
11098
|
console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
|
|
10463
11099
|
}
|
|
10464
|
-
const fullPath =
|
|
10465
|
-
const dir =
|
|
10466
|
-
const newPath = dir === "." ? `${sanitizedTitle}.md` :
|
|
10467
|
-
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);
|
|
10468
11104
|
try {
|
|
10469
|
-
await
|
|
11105
|
+
await fs21.access(fullPath);
|
|
10470
11106
|
} catch {
|
|
10471
11107
|
const result2 = {
|
|
10472
11108
|
success: false,
|
|
@@ -10477,7 +11113,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10477
11113
|
}
|
|
10478
11114
|
if (fullPath !== newFullPath) {
|
|
10479
11115
|
try {
|
|
10480
|
-
await
|
|
11116
|
+
await fs21.access(newFullPath);
|
|
10481
11117
|
const result2 = {
|
|
10482
11118
|
success: false,
|
|
10483
11119
|
message: `A note with this title already exists: ${newPath}`,
|
|
@@ -10487,7 +11123,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10487
11123
|
} catch {
|
|
10488
11124
|
}
|
|
10489
11125
|
}
|
|
10490
|
-
const sourceContent = await
|
|
11126
|
+
const sourceContent = await fs21.readFile(fullPath, "utf-8");
|
|
10491
11127
|
const parsed = matter6(sourceContent);
|
|
10492
11128
|
const aliases = extractAliases2(parsed.data);
|
|
10493
11129
|
const oldTitle = getTitleFromPath(notePath);
|
|
@@ -10516,7 +11152,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10516
11152
|
}
|
|
10517
11153
|
}
|
|
10518
11154
|
if (fullPath !== newFullPath) {
|
|
10519
|
-
await
|
|
11155
|
+
await fs21.rename(fullPath, newFullPath);
|
|
10520
11156
|
}
|
|
10521
11157
|
let gitCommit;
|
|
10522
11158
|
let undoAvailable;
|
|
@@ -10689,8 +11325,8 @@ init_schema();
|
|
|
10689
11325
|
|
|
10690
11326
|
// src/core/write/policy/parser.ts
|
|
10691
11327
|
init_schema();
|
|
10692
|
-
import
|
|
10693
|
-
import
|
|
11328
|
+
import fs22 from "fs/promises";
|
|
11329
|
+
import path21 from "path";
|
|
10694
11330
|
import matter7 from "gray-matter";
|
|
10695
11331
|
function parseYaml(content) {
|
|
10696
11332
|
const parsed = matter7(`---
|
|
@@ -10715,7 +11351,7 @@ function parsePolicyString(yamlContent) {
|
|
|
10715
11351
|
}
|
|
10716
11352
|
async function loadPolicyFile(filePath) {
|
|
10717
11353
|
try {
|
|
10718
|
-
const content = await
|
|
11354
|
+
const content = await fs22.readFile(filePath, "utf-8");
|
|
10719
11355
|
return parsePolicyString(content);
|
|
10720
11356
|
} catch (error) {
|
|
10721
11357
|
if (error.code === "ENOENT") {
|
|
@@ -10739,15 +11375,15 @@ async function loadPolicyFile(filePath) {
|
|
|
10739
11375
|
}
|
|
10740
11376
|
}
|
|
10741
11377
|
async function loadPolicy(vaultPath2, policyName) {
|
|
10742
|
-
const policiesDir =
|
|
10743
|
-
const policyPath =
|
|
11378
|
+
const policiesDir = path21.join(vaultPath2, ".claude", "policies");
|
|
11379
|
+
const policyPath = path21.join(policiesDir, `${policyName}.yaml`);
|
|
10744
11380
|
try {
|
|
10745
|
-
await
|
|
11381
|
+
await fs22.access(policyPath);
|
|
10746
11382
|
return loadPolicyFile(policyPath);
|
|
10747
11383
|
} catch {
|
|
10748
|
-
const ymlPath =
|
|
11384
|
+
const ymlPath = path21.join(policiesDir, `${policyName}.yml`);
|
|
10749
11385
|
try {
|
|
10750
|
-
await
|
|
11386
|
+
await fs22.access(ymlPath);
|
|
10751
11387
|
return loadPolicyFile(ymlPath);
|
|
10752
11388
|
} catch {
|
|
10753
11389
|
return {
|
|
@@ -10885,8 +11521,8 @@ init_template();
|
|
|
10885
11521
|
init_conditions();
|
|
10886
11522
|
init_schema();
|
|
10887
11523
|
init_writer();
|
|
10888
|
-
import
|
|
10889
|
-
import
|
|
11524
|
+
import fs24 from "fs/promises";
|
|
11525
|
+
import path23 from "path";
|
|
10890
11526
|
init_constants();
|
|
10891
11527
|
async function executeStep(step, vaultPath2, context, conditionResults) {
|
|
10892
11528
|
const { execute, reason } = shouldStepExecute(step.when, conditionResults);
|
|
@@ -10955,9 +11591,9 @@ async function executeAddToSection(params, vaultPath2, context) {
|
|
|
10955
11591
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
10956
11592
|
const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
|
|
10957
11593
|
const maxSuggestions = Number(params.maxSuggestions) || 3;
|
|
10958
|
-
const fullPath =
|
|
11594
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
10959
11595
|
try {
|
|
10960
|
-
await
|
|
11596
|
+
await fs24.access(fullPath);
|
|
10961
11597
|
} catch {
|
|
10962
11598
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
10963
11599
|
}
|
|
@@ -10995,9 +11631,9 @@ async function executeRemoveFromSection(params, vaultPath2) {
|
|
|
10995
11631
|
const pattern = String(params.pattern || "");
|
|
10996
11632
|
const mode = params.mode || "first";
|
|
10997
11633
|
const useRegex = Boolean(params.useRegex);
|
|
10998
|
-
const fullPath =
|
|
11634
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
10999
11635
|
try {
|
|
11000
|
-
await
|
|
11636
|
+
await fs24.access(fullPath);
|
|
11001
11637
|
} catch {
|
|
11002
11638
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11003
11639
|
}
|
|
@@ -11026,9 +11662,9 @@ async function executeReplaceInSection(params, vaultPath2, context) {
|
|
|
11026
11662
|
const mode = params.mode || "first";
|
|
11027
11663
|
const useRegex = Boolean(params.useRegex);
|
|
11028
11664
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
11029
|
-
const fullPath =
|
|
11665
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11030
11666
|
try {
|
|
11031
|
-
await
|
|
11667
|
+
await fs24.access(fullPath);
|
|
11032
11668
|
} catch {
|
|
11033
11669
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11034
11670
|
}
|
|
@@ -11069,16 +11705,16 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
11069
11705
|
if (!validatePath(vaultPath2, notePath)) {
|
|
11070
11706
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
11071
11707
|
}
|
|
11072
|
-
const fullPath =
|
|
11708
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11073
11709
|
try {
|
|
11074
|
-
await
|
|
11710
|
+
await fs24.access(fullPath);
|
|
11075
11711
|
if (!overwrite) {
|
|
11076
11712
|
return { success: false, message: `File already exists: ${notePath}`, path: notePath };
|
|
11077
11713
|
}
|
|
11078
11714
|
} catch {
|
|
11079
11715
|
}
|
|
11080
|
-
const dir =
|
|
11081
|
-
await
|
|
11716
|
+
const dir = path23.dirname(fullPath);
|
|
11717
|
+
await fs24.mkdir(dir, { recursive: true });
|
|
11082
11718
|
const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
|
|
11083
11719
|
await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
|
|
11084
11720
|
return {
|
|
@@ -11097,13 +11733,13 @@ async function executeDeleteNote(params, vaultPath2) {
|
|
|
11097
11733
|
if (!validatePath(vaultPath2, notePath)) {
|
|
11098
11734
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
11099
11735
|
}
|
|
11100
|
-
const fullPath =
|
|
11736
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11101
11737
|
try {
|
|
11102
|
-
await
|
|
11738
|
+
await fs24.access(fullPath);
|
|
11103
11739
|
} catch {
|
|
11104
11740
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11105
11741
|
}
|
|
11106
|
-
await
|
|
11742
|
+
await fs24.unlink(fullPath);
|
|
11107
11743
|
return {
|
|
11108
11744
|
success: true,
|
|
11109
11745
|
message: `Deleted note: ${notePath}`,
|
|
@@ -11114,9 +11750,9 @@ async function executeToggleTask(params, vaultPath2) {
|
|
|
11114
11750
|
const notePath = String(params.path || "");
|
|
11115
11751
|
const task = String(params.task || "");
|
|
11116
11752
|
const section = params.section ? String(params.section) : void 0;
|
|
11117
|
-
const fullPath =
|
|
11753
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11118
11754
|
try {
|
|
11119
|
-
await
|
|
11755
|
+
await fs24.access(fullPath);
|
|
11120
11756
|
} catch {
|
|
11121
11757
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11122
11758
|
}
|
|
@@ -11157,9 +11793,9 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
11157
11793
|
const completed = Boolean(params.completed);
|
|
11158
11794
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
11159
11795
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
11160
|
-
const fullPath =
|
|
11796
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11161
11797
|
try {
|
|
11162
|
-
await
|
|
11798
|
+
await fs24.access(fullPath);
|
|
11163
11799
|
} catch {
|
|
11164
11800
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11165
11801
|
}
|
|
@@ -11194,9 +11830,9 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
11194
11830
|
async function executeUpdateFrontmatter(params, vaultPath2) {
|
|
11195
11831
|
const notePath = String(params.path || "");
|
|
11196
11832
|
const updates = params.frontmatter || {};
|
|
11197
|
-
const fullPath =
|
|
11833
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11198
11834
|
try {
|
|
11199
|
-
await
|
|
11835
|
+
await fs24.access(fullPath);
|
|
11200
11836
|
} catch {
|
|
11201
11837
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11202
11838
|
}
|
|
@@ -11216,9 +11852,9 @@ async function executeAddFrontmatterField(params, vaultPath2) {
|
|
|
11216
11852
|
const notePath = String(params.path || "");
|
|
11217
11853
|
const key = String(params.key || "");
|
|
11218
11854
|
const value = params.value;
|
|
11219
|
-
const fullPath =
|
|
11855
|
+
const fullPath = path23.join(vaultPath2, notePath);
|
|
11220
11856
|
try {
|
|
11221
|
-
await
|
|
11857
|
+
await fs24.access(fullPath);
|
|
11222
11858
|
} catch {
|
|
11223
11859
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
11224
11860
|
}
|
|
@@ -11376,15 +12012,15 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
|
|
|
11376
12012
|
async function rollbackChanges(vaultPath2, originalContents, filesModified) {
|
|
11377
12013
|
for (const filePath of filesModified) {
|
|
11378
12014
|
const original = originalContents.get(filePath);
|
|
11379
|
-
const fullPath =
|
|
12015
|
+
const fullPath = path23.join(vaultPath2, filePath);
|
|
11380
12016
|
if (original === null) {
|
|
11381
12017
|
try {
|
|
11382
|
-
await
|
|
12018
|
+
await fs24.unlink(fullPath);
|
|
11383
12019
|
} catch {
|
|
11384
12020
|
}
|
|
11385
12021
|
} else if (original !== void 0) {
|
|
11386
12022
|
try {
|
|
11387
|
-
await
|
|
12023
|
+
await fs24.writeFile(fullPath, original);
|
|
11388
12024
|
} catch {
|
|
11389
12025
|
}
|
|
11390
12026
|
}
|
|
@@ -11430,27 +12066,27 @@ async function previewPolicy(policy, vaultPath2, variables) {
|
|
|
11430
12066
|
}
|
|
11431
12067
|
|
|
11432
12068
|
// src/core/write/policy/storage.ts
|
|
11433
|
-
import
|
|
11434
|
-
import
|
|
12069
|
+
import fs25 from "fs/promises";
|
|
12070
|
+
import path24 from "path";
|
|
11435
12071
|
function getPoliciesDir(vaultPath2) {
|
|
11436
|
-
return
|
|
12072
|
+
return path24.join(vaultPath2, ".claude", "policies");
|
|
11437
12073
|
}
|
|
11438
12074
|
async function ensurePoliciesDir(vaultPath2) {
|
|
11439
12075
|
const dir = getPoliciesDir(vaultPath2);
|
|
11440
|
-
await
|
|
12076
|
+
await fs25.mkdir(dir, { recursive: true });
|
|
11441
12077
|
}
|
|
11442
12078
|
async function listPolicies(vaultPath2) {
|
|
11443
12079
|
const dir = getPoliciesDir(vaultPath2);
|
|
11444
12080
|
const policies = [];
|
|
11445
12081
|
try {
|
|
11446
|
-
const files = await
|
|
12082
|
+
const files = await fs25.readdir(dir);
|
|
11447
12083
|
for (const file of files) {
|
|
11448
12084
|
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
|
|
11449
12085
|
continue;
|
|
11450
12086
|
}
|
|
11451
|
-
const filePath =
|
|
11452
|
-
const stat3 = await
|
|
11453
|
-
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");
|
|
11454
12090
|
const metadata = extractPolicyMetadata(content);
|
|
11455
12091
|
policies.push({
|
|
11456
12092
|
name: metadata.name || file.replace(/\.ya?ml$/, ""),
|
|
@@ -11473,10 +12109,10 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
|
|
|
11473
12109
|
const dir = getPoliciesDir(vaultPath2);
|
|
11474
12110
|
await ensurePoliciesDir(vaultPath2);
|
|
11475
12111
|
const filename = `${policyName}.yaml`;
|
|
11476
|
-
const filePath =
|
|
12112
|
+
const filePath = path24.join(dir, filename);
|
|
11477
12113
|
if (!overwrite) {
|
|
11478
12114
|
try {
|
|
11479
|
-
await
|
|
12115
|
+
await fs25.access(filePath);
|
|
11480
12116
|
return {
|
|
11481
12117
|
success: false,
|
|
11482
12118
|
path: filename,
|
|
@@ -11493,7 +12129,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
|
|
|
11493
12129
|
message: `Invalid policy: ${validation.errors.map((e) => e.message).join("; ")}`
|
|
11494
12130
|
};
|
|
11495
12131
|
}
|
|
11496
|
-
await
|
|
12132
|
+
await fs25.writeFile(filePath, content, "utf-8");
|
|
11497
12133
|
return {
|
|
11498
12134
|
success: true,
|
|
11499
12135
|
path: filename,
|
|
@@ -12016,8 +12652,8 @@ function registerPolicyTools(server2, vaultPath2) {
|
|
|
12016
12652
|
import { z as z19 } from "zod";
|
|
12017
12653
|
|
|
12018
12654
|
// src/core/write/tagRename.ts
|
|
12019
|
-
import * as
|
|
12020
|
-
import * as
|
|
12655
|
+
import * as fs26 from "fs/promises";
|
|
12656
|
+
import * as path25 from "path";
|
|
12021
12657
|
import matter8 from "gray-matter";
|
|
12022
12658
|
import { getProtectedZones } from "@velvetmonkey/vault-core";
|
|
12023
12659
|
function getNotesInFolder3(index, folder) {
|
|
@@ -12123,10 +12759,10 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
|
|
|
12123
12759
|
const previews = [];
|
|
12124
12760
|
let totalChanges = 0;
|
|
12125
12761
|
for (const note of affectedNotes) {
|
|
12126
|
-
const fullPath =
|
|
12762
|
+
const fullPath = path25.join(vaultPath2, note.path);
|
|
12127
12763
|
let fileContent;
|
|
12128
12764
|
try {
|
|
12129
|
-
fileContent = await
|
|
12765
|
+
fileContent = await fs26.readFile(fullPath, "utf-8");
|
|
12130
12766
|
} catch {
|
|
12131
12767
|
continue;
|
|
12132
12768
|
}
|
|
@@ -12186,385 +12822,987 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
|
|
|
12186
12822
|
fm.tags = newTags;
|
|
12187
12823
|
}
|
|
12188
12824
|
}
|
|
12189
|
-
const { content: updatedContent, changes: contentChanges } = replaceInlineTags(
|
|
12190
|
-
parsed.content,
|
|
12191
|
-
cleanOld,
|
|
12192
|
-
cleanNew,
|
|
12193
|
-
renameChildren
|
|
12194
|
-
);
|
|
12195
|
-
preview.content_changes = contentChanges;
|
|
12196
|
-
preview.total_changes = preview.frontmatter_changes.length + preview.content_changes.length;
|
|
12197
|
-
totalChanges += preview.total_changes;
|
|
12198
|
-
if (preview.total_changes > 0) {
|
|
12199
|
-
previews.push(preview);
|
|
12200
|
-
if (!dryRun) {
|
|
12201
|
-
const newContent = matter8.stringify(updatedContent, fm);
|
|
12202
|
-
await
|
|
12203
|
-
}
|
|
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);
|
|
13004
|
+
}
|
|
13005
|
+
}
|
|
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;
|
|
12204
13028
|
}
|
|
13029
|
+
wikilinkSuppressedCount = getSuppressedCount(stateDb2);
|
|
12205
13030
|
}
|
|
12206
13031
|
return {
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
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
|
|
12214
13043
|
};
|
|
12215
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
|
+
}
|
|
12216
13125
|
|
|
12217
|
-
// src/tools/
|
|
12218
|
-
function
|
|
13126
|
+
// src/tools/read/metrics.ts
|
|
13127
|
+
function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
12219
13128
|
server2.registerTool(
|
|
12220
|
-
"
|
|
13129
|
+
"vault_growth",
|
|
12221
13130
|
{
|
|
12222
|
-
title: "
|
|
12223
|
-
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.',
|
|
12224
13133
|
inputSchema: {
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
dry_run: z19.boolean().optional().describe("Preview only, no changes (default: true)"),
|
|
12230
|
-
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)")
|
|
12231
13138
|
}
|
|
12232
13139
|
},
|
|
12233
|
-
async ({
|
|
13140
|
+
async ({ mode, metric, days_back, limit: eventLimit }) => {
|
|
12234
13141
|
const index = getIndex();
|
|
12235
|
-
const
|
|
12236
|
-
const
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
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
|
+
}
|
|
12242
13197
|
return {
|
|
12243
13198
|
content: [
|
|
12244
13199
|
{
|
|
12245
13200
|
type: "text",
|
|
12246
13201
|
text: JSON.stringify(result, null, 2)
|
|
12247
13202
|
}
|
|
12248
|
-
]
|
|
12249
|
-
};
|
|
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
|
+
}
|
|
12250
13317
|
}
|
|
12251
|
-
|
|
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;
|
|
12252
13360
|
}
|
|
12253
13361
|
|
|
12254
|
-
// src/tools/
|
|
12255
|
-
|
|
12256
|
-
function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
13362
|
+
// src/tools/read/activity.ts
|
|
13363
|
+
function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
12257
13364
|
server2.registerTool(
|
|
12258
|
-
"
|
|
13365
|
+
"vault_activity",
|
|
12259
13366
|
{
|
|
12260
|
-
title: "
|
|
12261
|
-
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)',
|
|
12262
13369
|
inputSchema: {
|
|
12263
|
-
mode:
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
correct: z20.boolean().optional().describe("Whether the wikilink was correct (for report mode)"),
|
|
12268
|
-
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)")
|
|
12269
13374
|
}
|
|
12270
13375
|
},
|
|
12271
|
-
async ({ mode,
|
|
13376
|
+
async ({ mode, session_id, days_back, limit: resultLimit }) => {
|
|
12272
13377
|
const stateDb2 = getStateDb();
|
|
12273
13378
|
if (!stateDb2) {
|
|
12274
13379
|
return {
|
|
12275
13380
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
12276
13381
|
};
|
|
12277
13382
|
}
|
|
12278
|
-
|
|
13383
|
+
const daysBack = days_back ?? 30;
|
|
13384
|
+
const limit = resultLimit ?? 20;
|
|
12279
13385
|
switch (mode) {
|
|
12280
|
-
case "
|
|
12281
|
-
|
|
13386
|
+
case "session": {
|
|
13387
|
+
const sid = session_id ?? getSessionId2();
|
|
13388
|
+
if (!sid) {
|
|
12282
13389
|
return {
|
|
12283
|
-
content: [{ type: "text", text: JSON.stringify({ error: "
|
|
13390
|
+
content: [{ type: "text", text: JSON.stringify({ error: "No session ID available" }) }]
|
|
12284
13391
|
};
|
|
12285
13392
|
}
|
|
12286
|
-
|
|
12287
|
-
const
|
|
12288
|
-
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
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) }]
|
|
12296
13403
|
};
|
|
12297
|
-
break;
|
|
12298
13404
|
}
|
|
12299
|
-
case "
|
|
12300
|
-
const
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
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) }]
|
|
12305
13412
|
};
|
|
12306
|
-
break;
|
|
12307
13413
|
}
|
|
12308
|
-
case "
|
|
12309
|
-
const
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
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) }]
|
|
12315
13432
|
};
|
|
12316
|
-
break;
|
|
12317
13433
|
}
|
|
12318
13434
|
}
|
|
12319
|
-
return {
|
|
12320
|
-
content: [
|
|
12321
|
-
{
|
|
12322
|
-
type: "text",
|
|
12323
|
-
text: JSON.stringify(result, null, 2)
|
|
12324
|
-
}
|
|
12325
|
-
]
|
|
12326
|
-
};
|
|
12327
13435
|
}
|
|
12328
13436
|
);
|
|
12329
13437
|
}
|
|
12330
13438
|
|
|
12331
|
-
// src/tools/read/
|
|
12332
|
-
import { z as
|
|
13439
|
+
// src/tools/read/similarity.ts
|
|
13440
|
+
import { z as z23 } from "zod";
|
|
12333
13441
|
|
|
12334
|
-
// src/core/
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
"
|
|
12339
|
-
"
|
|
12340
|
-
"
|
|
12341
|
-
"
|
|
12342
|
-
"
|
|
12343
|
-
"
|
|
12344
|
-
"
|
|
12345
|
-
"
|
|
12346
|
-
"
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
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 [];
|
|
12376
13587
|
}
|
|
12377
|
-
const
|
|
12378
|
-
|
|
12379
|
-
const
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
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
|
+
}
|
|
12392
13619
|
}
|
|
12393
|
-
|
|
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 [];
|
|
12394
13628
|
}
|
|
12395
|
-
return {
|
|
12396
|
-
note_count: noteCount,
|
|
12397
|
-
link_count: linkCount,
|
|
12398
|
-
orphan_count: orphanCount,
|
|
12399
|
-
tag_count: tagCount,
|
|
12400
|
-
entity_count: entityCount,
|
|
12401
|
-
avg_links_per_note: Math.round(avgLinksPerNote * 100) / 100,
|
|
12402
|
-
link_density: Math.round(linkDensity * 1e4) / 1e4,
|
|
12403
|
-
connected_ratio: Math.round(connectedRatio * 1e3) / 1e3,
|
|
12404
|
-
wikilink_accuracy: wikilinkAccuracy,
|
|
12405
|
-
wikilink_feedback_volume: wikilinkFeedbackVolume,
|
|
12406
|
-
wikilink_suppressed_count: wikilinkSuppressedCount
|
|
12407
|
-
};
|
|
12408
|
-
}
|
|
12409
|
-
function recordMetrics(stateDb2, metrics) {
|
|
12410
|
-
const timestamp = Date.now();
|
|
12411
|
-
const insert = stateDb2.db.prepare(
|
|
12412
|
-
"INSERT INTO vault_metrics (timestamp, metric, value) VALUES (?, ?, ?)"
|
|
12413
|
-
);
|
|
12414
|
-
const transaction = stateDb2.db.transaction(() => {
|
|
12415
|
-
for (const [metric, value] of Object.entries(metrics)) {
|
|
12416
|
-
insert.run(timestamp, metric, value);
|
|
12417
|
-
}
|
|
12418
|
-
});
|
|
12419
|
-
transaction();
|
|
12420
13629
|
}
|
|
12421
|
-
function
|
|
12422
|
-
const
|
|
12423
|
-
|
|
12424
|
-
if (
|
|
12425
|
-
|
|
12426
|
-
|
|
12427
|
-
).
|
|
12428
|
-
}
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
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
|
|
12437
13658
|
}));
|
|
12438
13659
|
}
|
|
12439
|
-
function
|
|
12440
|
-
const
|
|
12441
|
-
const
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
|
|
12450
|
-
}
|
|
12451
|
-
if (previousValues.size === 0) {
|
|
12452
|
-
const fallbackRows = stateDb2.db.prepare(`
|
|
12453
|
-
SELECT metric, MIN(value) as value FROM vault_metrics
|
|
12454
|
-
WHERE timestamp >= ?
|
|
12455
|
-
GROUP BY metric
|
|
12456
|
-
HAVING timestamp = MIN(timestamp)
|
|
12457
|
-
`).all(cutoff);
|
|
12458
|
-
for (const row of fallbackRows) {
|
|
12459
|
-
previousValues.set(row.metric, row.value);
|
|
12460
|
-
}
|
|
12461
|
-
}
|
|
12462
|
-
const trends = [];
|
|
12463
|
-
for (const metricName of ALL_METRICS) {
|
|
12464
|
-
const current = currentMetrics[metricName] ?? 0;
|
|
12465
|
-
const previous = previousValues.get(metricName) ?? current;
|
|
12466
|
-
const delta = current - previous;
|
|
12467
|
-
const deltaPct = previous !== 0 ? Math.round(delta / previous * 1e4) / 100 : delta !== 0 ? 100 : 0;
|
|
12468
|
-
let direction = "stable";
|
|
12469
|
-
if (delta > 0) direction = "up";
|
|
12470
|
-
if (delta < 0) direction = "down";
|
|
12471
|
-
trends.push({
|
|
12472
|
-
metric: metricName,
|
|
12473
|
-
current,
|
|
12474
|
-
previous,
|
|
12475
|
-
delta,
|
|
12476
|
-
delta_percent: deltaPct,
|
|
12477
|
-
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
|
|
12478
13671
|
});
|
|
13672
|
+
} catch {
|
|
13673
|
+
return bm25Results.slice(0, limit);
|
|
12479
13674
|
}
|
|
12480
|
-
|
|
12481
|
-
}
|
|
12482
|
-
|
|
12483
|
-
|
|
12484
|
-
const
|
|
12485
|
-
|
|
12486
|
-
).
|
|
12487
|
-
|
|
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);
|
|
12488
13694
|
}
|
|
12489
13695
|
|
|
12490
|
-
// src/tools/read/
|
|
12491
|
-
function
|
|
13696
|
+
// src/tools/read/similarity.ts
|
|
13697
|
+
function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
12492
13698
|
server2.registerTool(
|
|
12493
|
-
"
|
|
13699
|
+
"find_similar",
|
|
12494
13700
|
{
|
|
12495
|
-
title: "
|
|
12496
|
-
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.",
|
|
12497
13703
|
inputSchema: {
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
limit: z21.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
|
|
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)")
|
|
12502
13707
|
}
|
|
12503
13708
|
},
|
|
12504
|
-
async ({
|
|
13709
|
+
async ({ path: path28, limit, exclude_linked }) => {
|
|
12505
13710
|
const index = getIndex();
|
|
13711
|
+
const vaultPath2 = getVaultPath();
|
|
12506
13712
|
const stateDb2 = getStateDb();
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
const metrics = computeMetrics(index, stateDb2 ?? void 0);
|
|
12512
|
-
result = {
|
|
12513
|
-
mode: "current",
|
|
12514
|
-
metrics,
|
|
12515
|
-
recorded_at: Date.now()
|
|
12516
|
-
};
|
|
12517
|
-
break;
|
|
12518
|
-
}
|
|
12519
|
-
case "history": {
|
|
12520
|
-
if (!stateDb2) {
|
|
12521
|
-
return {
|
|
12522
|
-
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for historical queries" }) }]
|
|
12523
|
-
};
|
|
12524
|
-
}
|
|
12525
|
-
const history = getMetricHistory(stateDb2, metric, daysBack);
|
|
12526
|
-
result = {
|
|
12527
|
-
mode: "history",
|
|
12528
|
-
history
|
|
12529
|
-
};
|
|
12530
|
-
break;
|
|
12531
|
-
}
|
|
12532
|
-
case "trends": {
|
|
12533
|
-
if (!stateDb2) {
|
|
12534
|
-
return {
|
|
12535
|
-
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for trend analysis" }) }]
|
|
12536
|
-
};
|
|
12537
|
-
}
|
|
12538
|
-
const currentMetrics = computeMetrics(index, stateDb2);
|
|
12539
|
-
const trends = computeTrends(stateDb2, currentMetrics, daysBack);
|
|
12540
|
-
result = {
|
|
12541
|
-
mode: "trends",
|
|
12542
|
-
trends
|
|
12543
|
-
};
|
|
12544
|
-
break;
|
|
12545
|
-
}
|
|
12546
|
-
case "index_activity": {
|
|
12547
|
-
if (!stateDb2) {
|
|
12548
|
-
return {
|
|
12549
|
-
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for index activity queries" }) }]
|
|
12550
|
-
};
|
|
12551
|
-
}
|
|
12552
|
-
const summary = getIndexActivitySummary(stateDb2);
|
|
12553
|
-
const recentEvents = getRecentIndexEvents(stateDb2, eventLimit ?? 20);
|
|
12554
|
-
result = {
|
|
12555
|
-
mode: "index_activity",
|
|
12556
|
-
index_activity: { summary, recent_events: recentEvents }
|
|
12557
|
-
};
|
|
12558
|
-
break;
|
|
12559
|
-
}
|
|
13713
|
+
if (!stateDb2) {
|
|
13714
|
+
return {
|
|
13715
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
13716
|
+
};
|
|
12560
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
|
+
};
|
|
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);
|
|
12561
13733
|
return {
|
|
12562
|
-
content: [
|
|
12563
|
-
|
|
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: [{
|
|
12564
13768
|
type: "text",
|
|
12565
|
-
text: JSON.stringify(
|
|
12566
|
-
}
|
|
12567
|
-
|
|
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
|
+
}]
|
|
12568
13806
|
};
|
|
12569
13807
|
}
|
|
12570
13808
|
);
|
|
@@ -12763,6 +14001,7 @@ var TOOL_CATEGORY = {
|
|
|
12763
14001
|
get_unlinked_mentions: "health",
|
|
12764
14002
|
// search (unified: metadata + content + entities)
|
|
12765
14003
|
search: "search",
|
|
14004
|
+
init_semantic: "search",
|
|
12766
14005
|
// backlinks
|
|
12767
14006
|
get_backlinks: "backlinks",
|
|
12768
14007
|
get_forward_links: "backlinks",
|
|
@@ -12809,7 +14048,11 @@ var TOOL_CATEGORY = {
|
|
|
12809
14048
|
// health (growth metrics)
|
|
12810
14049
|
vault_growth: "health",
|
|
12811
14050
|
// wikilinks (feedback)
|
|
12812
|
-
wikilink_feedback: "wikilinks"
|
|
14051
|
+
wikilink_feedback: "wikilinks",
|
|
14052
|
+
// health (activity tracking)
|
|
14053
|
+
vault_activity: "health",
|
|
14054
|
+
// schema (content similarity)
|
|
14055
|
+
find_similar: "schema"
|
|
12813
14056
|
};
|
|
12814
14057
|
var server = new McpServer({
|
|
12815
14058
|
name: "flywheel-memory",
|
|
@@ -12826,15 +14069,62 @@ function gateByCategory(name) {
|
|
|
12826
14069
|
_registeredCount++;
|
|
12827
14070
|
return true;
|
|
12828
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
|
+
}
|
|
12829
14113
|
var _originalTool = server.tool.bind(server);
|
|
12830
14114
|
server.tool = (name, ...args) => {
|
|
12831
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
|
+
}
|
|
12832
14119
|
return _originalTool(name, ...args);
|
|
12833
14120
|
};
|
|
12834
14121
|
var _originalRegisterTool = server.registerTool?.bind(server);
|
|
12835
14122
|
if (_originalRegisterTool) {
|
|
12836
14123
|
server.registerTool = (name, ...args) => {
|
|
12837
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
|
+
}
|
|
12838
14128
|
return _originalRegisterTool(name, ...args);
|
|
12839
14129
|
};
|
|
12840
14130
|
}
|
|
@@ -12857,7 +14147,7 @@ registerGraphTools(server, () => vaultIndex, () => vaultPath);
|
|
|
12857
14147
|
registerWikilinkTools(server, () => vaultIndex, () => vaultPath);
|
|
12858
14148
|
registerQueryTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
12859
14149
|
registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
|
|
12860
|
-
registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath);
|
|
14150
|
+
registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
12861
14151
|
registerVaultSchemaTools(server, () => vaultIndex, () => vaultPath);
|
|
12862
14152
|
registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath);
|
|
12863
14153
|
registerMigrationTools(server, () => vaultIndex, () => vaultPath);
|
|
@@ -12871,6 +14161,15 @@ registerPolicyTools(server, vaultPath);
|
|
|
12871
14161
|
registerTagTools(server, () => vaultIndex, () => vaultPath);
|
|
12872
14162
|
registerWikilinkFeedbackTools(server, () => stateDb);
|
|
12873
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);
|
|
12874
14173
|
registerVaultResources(server, () => vaultIndex ?? null);
|
|
12875
14174
|
console.error(`[Memory] Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
12876
14175
|
async function main() {
|
|
@@ -12881,6 +14180,7 @@ async function main() {
|
|
|
12881
14180
|
stateDb = openStateDb(vaultPath);
|
|
12882
14181
|
console.error("[Memory] StateDb initialized");
|
|
12883
14182
|
setFTS5Database(stateDb.db);
|
|
14183
|
+
setEmbeddingsDatabase(stateDb.db);
|
|
12884
14184
|
setWriteStateDb(stateDb);
|
|
12885
14185
|
await initializeEntityIndex(vaultPath);
|
|
12886
14186
|
} catch (err) {
|
|
@@ -13005,11 +14305,21 @@ async function runPostIndexWork(index) {
|
|
|
13005
14305
|
recordMetrics(stateDb, metrics);
|
|
13006
14306
|
purgeOldMetrics(stateDb, 90);
|
|
13007
14307
|
purgeOldIndexEvents(stateDb, 90);
|
|
14308
|
+
purgeOldInvocations(stateDb, 90);
|
|
13008
14309
|
console.error("[Memory] Growth metrics recorded");
|
|
13009
14310
|
} catch (err) {
|
|
13010
14311
|
console.error("[Memory] Failed to record metrics:", err);
|
|
13011
14312
|
}
|
|
13012
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
|
+
}
|
|
13013
14323
|
if (stateDb) {
|
|
13014
14324
|
try {
|
|
13015
14325
|
updateSuppressionList(stateDb);
|
|
@@ -13052,6 +14362,19 @@ async function runPostIndexWork(index) {
|
|
|
13052
14362
|
}
|
|
13053
14363
|
await updateEntitiesInStateDb();
|
|
13054
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
|
+
}
|
|
13055
14378
|
if (stateDb) {
|
|
13056
14379
|
try {
|
|
13057
14380
|
saveVaultIndexToCache(stateDb, vaultIndex);
|
|
@@ -13088,10 +14411,32 @@ async function runPostIndexWork(index) {
|
|
|
13088
14411
|
watcher.start();
|
|
13089
14412
|
}
|
|
13090
14413
|
}
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
});
|
|
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
|
+
}
|
|
13095
14440
|
process.on("beforeExit", async () => {
|
|
13096
14441
|
await flushLogs();
|
|
13097
14442
|
});
|