@velvetmonkey/flywheel-memory 2.0.2 → 2.0.3
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 +488 -320
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ var init_constants = __esm({
|
|
|
24
24
|
|
|
25
25
|
// src/core/write/writer.ts
|
|
26
26
|
import fs14 from "fs/promises";
|
|
27
|
-
import
|
|
27
|
+
import path15 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 = path15.resolve(vaultPath2);
|
|
349
|
+
const resolvedNote = path15.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 = path15.resolve(vaultPath2);
|
|
378
|
+
const resolvedNote = path15.resolve(vaultPath2, notePath);
|
|
379
379
|
if (!resolvedNote.startsWith(resolvedVault)) {
|
|
380
380
|
return {
|
|
381
381
|
valid: false,
|
|
@@ -389,7 +389,7 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
389
389
|
};
|
|
390
390
|
}
|
|
391
391
|
try {
|
|
392
|
-
const fullPath =
|
|
392
|
+
const fullPath = path15.join(vaultPath2, notePath);
|
|
393
393
|
try {
|
|
394
394
|
await fs14.access(fullPath);
|
|
395
395
|
const realPath = await fs14.realpath(fullPath);
|
|
@@ -400,7 +400,7 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
400
400
|
reason: "Symlink target is outside vault"
|
|
401
401
|
};
|
|
402
402
|
}
|
|
403
|
-
const relativePath =
|
|
403
|
+
const relativePath = path15.relative(realVaultPath, realPath);
|
|
404
404
|
if (isSensitivePath(relativePath)) {
|
|
405
405
|
return {
|
|
406
406
|
valid: false,
|
|
@@ -408,7 +408,7 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
408
408
|
};
|
|
409
409
|
}
|
|
410
410
|
} catch {
|
|
411
|
-
const parentDir =
|
|
411
|
+
const parentDir = path15.dirname(fullPath);
|
|
412
412
|
try {
|
|
413
413
|
await fs14.access(parentDir);
|
|
414
414
|
const realParentPath = await fs14.realpath(parentDir);
|
|
@@ -434,7 +434,7 @@ 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 =
|
|
437
|
+
const fullPath = path15.join(vaultPath2, notePath);
|
|
438
438
|
const rawContent = await fs14.readFile(fullPath, "utf-8");
|
|
439
439
|
const lineEnding = detectLineEnding(rawContent);
|
|
440
440
|
const normalizedContent = normalizeLineEndings(rawContent);
|
|
@@ -483,7 +483,7 @@ 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 = path15.join(vaultPath2, notePath);
|
|
487
487
|
let output = matter5.stringify(content, frontmatter);
|
|
488
488
|
output = normalizeTrailingNewline(output);
|
|
489
489
|
output = convertLineEndings(output, lineEnding);
|
|
@@ -710,8 +710,8 @@ function createContext(variables = {}) {
|
|
|
710
710
|
}
|
|
711
711
|
};
|
|
712
712
|
}
|
|
713
|
-
function resolvePath(obj,
|
|
714
|
-
const parts =
|
|
713
|
+
function resolvePath(obj, path24) {
|
|
714
|
+
const parts = path24.split(".");
|
|
715
715
|
let current = obj;
|
|
716
716
|
for (const part of parts) {
|
|
717
717
|
if (current === void 0 || current === null) {
|
|
@@ -1150,7 +1150,7 @@ __export(conditions_exports, {
|
|
|
1150
1150
|
shouldStepExecute: () => shouldStepExecute
|
|
1151
1151
|
});
|
|
1152
1152
|
import fs20 from "fs/promises";
|
|
1153
|
-
import
|
|
1153
|
+
import path21 from "path";
|
|
1154
1154
|
async function evaluateCondition(condition, vaultPath2, context) {
|
|
1155
1155
|
const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
|
|
1156
1156
|
const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
|
|
@@ -1203,7 +1203,7 @@ async function evaluateCondition(condition, vaultPath2, context) {
|
|
|
1203
1203
|
}
|
|
1204
1204
|
}
|
|
1205
1205
|
async function evaluateFileExists(vaultPath2, notePath, expectExists) {
|
|
1206
|
-
const fullPath =
|
|
1206
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
1207
1207
|
try {
|
|
1208
1208
|
await fs20.access(fullPath);
|
|
1209
1209
|
return {
|
|
@@ -1218,7 +1218,7 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
|
|
|
1218
1218
|
}
|
|
1219
1219
|
}
|
|
1220
1220
|
async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
|
|
1221
|
-
const fullPath =
|
|
1221
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
1222
1222
|
try {
|
|
1223
1223
|
await fs20.access(fullPath);
|
|
1224
1224
|
} catch {
|
|
@@ -1249,7 +1249,7 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
|
|
|
1249
1249
|
}
|
|
1250
1250
|
}
|
|
1251
1251
|
async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
|
|
1252
|
-
const fullPath =
|
|
1252
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
1253
1253
|
try {
|
|
1254
1254
|
await fs20.access(fullPath);
|
|
1255
1255
|
} catch {
|
|
@@ -1280,7 +1280,7 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
|
|
|
1280
1280
|
}
|
|
1281
1281
|
}
|
|
1282
1282
|
async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
|
|
1283
|
-
const fullPath =
|
|
1283
|
+
const fullPath = path21.join(vaultPath2, notePath);
|
|
1284
1284
|
try {
|
|
1285
1285
|
await fs20.access(fullPath);
|
|
1286
1286
|
} catch {
|
|
@@ -1689,8 +1689,8 @@ function updateIndexProgress(parsed, total) {
|
|
|
1689
1689
|
function normalizeTarget(target) {
|
|
1690
1690
|
return target.toLowerCase().replace(/\.md$/, "");
|
|
1691
1691
|
}
|
|
1692
|
-
function normalizeNotePath(
|
|
1693
|
-
return
|
|
1692
|
+
function normalizeNotePath(path24) {
|
|
1693
|
+
return path24.toLowerCase().replace(/\.md$/, "");
|
|
1694
1694
|
}
|
|
1695
1695
|
async function buildVaultIndex(vaultPath2, options = {}) {
|
|
1696
1696
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
|
|
@@ -1884,7 +1884,7 @@ function findSimilarEntity(index, target) {
|
|
|
1884
1884
|
}
|
|
1885
1885
|
const maxDist = normalizedLen <= 10 ? 1 : 2;
|
|
1886
1886
|
let bestMatch;
|
|
1887
|
-
for (const [entity,
|
|
1887
|
+
for (const [entity, path24] of index.entities) {
|
|
1888
1888
|
const lenDiff = Math.abs(entity.length - normalizedLen);
|
|
1889
1889
|
if (lenDiff > maxDist) {
|
|
1890
1890
|
continue;
|
|
@@ -1892,7 +1892,7 @@ function findSimilarEntity(index, target) {
|
|
|
1892
1892
|
const dist = levenshteinDistance(normalized, entity);
|
|
1893
1893
|
if (dist > 0 && dist <= maxDist) {
|
|
1894
1894
|
if (!bestMatch || dist < bestMatch.distance) {
|
|
1895
|
-
bestMatch = { path:
|
|
1895
|
+
bestMatch = { path: path24, entity, distance: dist };
|
|
1896
1896
|
if (dist === 1) {
|
|
1897
1897
|
return bestMatch;
|
|
1898
1898
|
}
|
|
@@ -2322,30 +2322,30 @@ var EventQueue = class {
|
|
|
2322
2322
|
* Add a new event to the queue
|
|
2323
2323
|
*/
|
|
2324
2324
|
push(type, rawPath) {
|
|
2325
|
-
const
|
|
2325
|
+
const path24 = normalizePath(rawPath);
|
|
2326
2326
|
const now = Date.now();
|
|
2327
2327
|
const event = {
|
|
2328
2328
|
type,
|
|
2329
|
-
path:
|
|
2329
|
+
path: path24,
|
|
2330
2330
|
timestamp: now
|
|
2331
2331
|
};
|
|
2332
|
-
let pending = this.pending.get(
|
|
2332
|
+
let pending = this.pending.get(path24);
|
|
2333
2333
|
if (!pending) {
|
|
2334
2334
|
pending = {
|
|
2335
2335
|
events: [],
|
|
2336
2336
|
timer: null,
|
|
2337
2337
|
lastEvent: now
|
|
2338
2338
|
};
|
|
2339
|
-
this.pending.set(
|
|
2339
|
+
this.pending.set(path24, pending);
|
|
2340
2340
|
}
|
|
2341
2341
|
pending.events.push(event);
|
|
2342
2342
|
pending.lastEvent = now;
|
|
2343
|
-
console.error(`[flywheel] QUEUE: pushed ${type} for ${
|
|
2343
|
+
console.error(`[flywheel] QUEUE: pushed ${type} for ${path24}, pending=${this.pending.size}`);
|
|
2344
2344
|
if (pending.timer) {
|
|
2345
2345
|
clearTimeout(pending.timer);
|
|
2346
2346
|
}
|
|
2347
2347
|
pending.timer = setTimeout(() => {
|
|
2348
|
-
this.flushPath(
|
|
2348
|
+
this.flushPath(path24);
|
|
2349
2349
|
}, this.config.debounceMs);
|
|
2350
2350
|
if (this.pending.size >= this.config.batchSize) {
|
|
2351
2351
|
this.flush();
|
|
@@ -2366,10 +2366,10 @@ var EventQueue = class {
|
|
|
2366
2366
|
/**
|
|
2367
2367
|
* Flush a single path's events
|
|
2368
2368
|
*/
|
|
2369
|
-
flushPath(
|
|
2370
|
-
const pending = this.pending.get(
|
|
2369
|
+
flushPath(path24) {
|
|
2370
|
+
const pending = this.pending.get(path24);
|
|
2371
2371
|
if (!pending || pending.events.length === 0) return;
|
|
2372
|
-
console.error(`[flywheel] QUEUE: flushing ${
|
|
2372
|
+
console.error(`[flywheel] QUEUE: flushing ${path24}, events=${pending.events.length}`);
|
|
2373
2373
|
if (pending.timer) {
|
|
2374
2374
|
clearTimeout(pending.timer);
|
|
2375
2375
|
pending.timer = null;
|
|
@@ -2378,7 +2378,7 @@ var EventQueue = class {
|
|
|
2378
2378
|
if (coalescedType) {
|
|
2379
2379
|
const coalesced = {
|
|
2380
2380
|
type: coalescedType,
|
|
2381
|
-
path:
|
|
2381
|
+
path: path24,
|
|
2382
2382
|
originalEvents: [...pending.events]
|
|
2383
2383
|
};
|
|
2384
2384
|
this.onBatch({
|
|
@@ -2386,7 +2386,7 @@ var EventQueue = class {
|
|
|
2386
2386
|
timestamp: Date.now()
|
|
2387
2387
|
});
|
|
2388
2388
|
}
|
|
2389
|
-
this.pending.delete(
|
|
2389
|
+
this.pending.delete(path24);
|
|
2390
2390
|
}
|
|
2391
2391
|
/**
|
|
2392
2392
|
* Flush all pending events
|
|
@@ -2398,7 +2398,7 @@ var EventQueue = class {
|
|
|
2398
2398
|
}
|
|
2399
2399
|
if (this.pending.size === 0) return;
|
|
2400
2400
|
const events = [];
|
|
2401
|
-
for (const [
|
|
2401
|
+
for (const [path24, pending] of this.pending) {
|
|
2402
2402
|
if (pending.timer) {
|
|
2403
2403
|
clearTimeout(pending.timer);
|
|
2404
2404
|
}
|
|
@@ -2406,7 +2406,7 @@ var EventQueue = class {
|
|
|
2406
2406
|
if (coalescedType) {
|
|
2407
2407
|
events.push({
|
|
2408
2408
|
type: coalescedType,
|
|
2409
|
-
path:
|
|
2409
|
+
path: path24,
|
|
2410
2410
|
originalEvents: [...pending.events]
|
|
2411
2411
|
});
|
|
2412
2412
|
}
|
|
@@ -2555,31 +2555,31 @@ function createVaultWatcher(options) {
|
|
|
2555
2555
|
usePolling: config.usePolling,
|
|
2556
2556
|
interval: config.usePolling ? config.pollInterval : void 0
|
|
2557
2557
|
});
|
|
2558
|
-
watcher.on("add", (
|
|
2559
|
-
console.error(`[flywheel] RAW EVENT: add ${
|
|
2560
|
-
if (shouldWatch(
|
|
2561
|
-
console.error(`[flywheel] ACCEPTED: add ${
|
|
2562
|
-
eventQueue.push("add",
|
|
2558
|
+
watcher.on("add", (path24) => {
|
|
2559
|
+
console.error(`[flywheel] RAW EVENT: add ${path24}`);
|
|
2560
|
+
if (shouldWatch(path24, vaultPath2)) {
|
|
2561
|
+
console.error(`[flywheel] ACCEPTED: add ${path24}`);
|
|
2562
|
+
eventQueue.push("add", path24);
|
|
2563
2563
|
} else {
|
|
2564
|
-
console.error(`[flywheel] FILTERED: add ${
|
|
2564
|
+
console.error(`[flywheel] FILTERED: add ${path24}`);
|
|
2565
2565
|
}
|
|
2566
2566
|
});
|
|
2567
|
-
watcher.on("change", (
|
|
2568
|
-
console.error(`[flywheel] RAW EVENT: change ${
|
|
2569
|
-
if (shouldWatch(
|
|
2570
|
-
console.error(`[flywheel] ACCEPTED: change ${
|
|
2571
|
-
eventQueue.push("change",
|
|
2567
|
+
watcher.on("change", (path24) => {
|
|
2568
|
+
console.error(`[flywheel] RAW EVENT: change ${path24}`);
|
|
2569
|
+
if (shouldWatch(path24, vaultPath2)) {
|
|
2570
|
+
console.error(`[flywheel] ACCEPTED: change ${path24}`);
|
|
2571
|
+
eventQueue.push("change", path24);
|
|
2572
2572
|
} else {
|
|
2573
|
-
console.error(`[flywheel] FILTERED: change ${
|
|
2573
|
+
console.error(`[flywheel] FILTERED: change ${path24}`);
|
|
2574
2574
|
}
|
|
2575
2575
|
});
|
|
2576
|
-
watcher.on("unlink", (
|
|
2577
|
-
console.error(`[flywheel] RAW EVENT: unlink ${
|
|
2578
|
-
if (shouldWatch(
|
|
2579
|
-
console.error(`[flywheel] ACCEPTED: unlink ${
|
|
2580
|
-
eventQueue.push("unlink",
|
|
2576
|
+
watcher.on("unlink", (path24) => {
|
|
2577
|
+
console.error(`[flywheel] RAW EVENT: unlink ${path24}`);
|
|
2578
|
+
if (shouldWatch(path24, vaultPath2)) {
|
|
2579
|
+
console.error(`[flywheel] ACCEPTED: unlink ${path24}`);
|
|
2580
|
+
eventQueue.push("unlink", path24);
|
|
2581
2581
|
} else {
|
|
2582
|
-
console.error(`[flywheel] FILTERED: unlink ${
|
|
2582
|
+
console.error(`[flywheel] FILTERED: unlink ${path24}`);
|
|
2583
2583
|
}
|
|
2584
2584
|
});
|
|
2585
2585
|
watcher.on("ready", () => {
|
|
@@ -2689,7 +2689,10 @@ import {
|
|
|
2689
2689
|
applyWikilinks,
|
|
2690
2690
|
resolveAliasWikilinks,
|
|
2691
2691
|
getEntityIndexFromDb,
|
|
2692
|
-
getStateDbMetadata
|
|
2692
|
+
getStateDbMetadata,
|
|
2693
|
+
getEntityByName,
|
|
2694
|
+
getEntitiesByAlias,
|
|
2695
|
+
searchEntities as searchEntitiesDb
|
|
2693
2696
|
} from "@velvetmonkey/vault-core";
|
|
2694
2697
|
|
|
2695
2698
|
// src/core/write/git.ts
|
|
@@ -4532,6 +4535,7 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
4532
4535
|
if (SUGGESTION_PATTERN.test(content)) {
|
|
4533
4536
|
return emptyResult;
|
|
4534
4537
|
}
|
|
4538
|
+
checkAndRefreshIfStale();
|
|
4535
4539
|
if (!isEntityIndexReady() || !entityIndex) {
|
|
4536
4540
|
return emptyResult;
|
|
4537
4541
|
}
|
|
@@ -4650,6 +4654,126 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
4650
4654
|
suffix
|
|
4651
4655
|
};
|
|
4652
4656
|
}
|
|
4657
|
+
function detectAliasCollisions(noteName, aliases = []) {
|
|
4658
|
+
if (!moduleStateDb4) return [];
|
|
4659
|
+
const collisions = [];
|
|
4660
|
+
const nameAsAlias = getEntitiesByAlias(moduleStateDb4, noteName);
|
|
4661
|
+
for (const entity of nameAsAlias) {
|
|
4662
|
+
if (entity.name.toLowerCase() === noteName.toLowerCase()) continue;
|
|
4663
|
+
collisions.push({
|
|
4664
|
+
term: noteName,
|
|
4665
|
+
source: "name",
|
|
4666
|
+
collidedWith: {
|
|
4667
|
+
name: entity.name,
|
|
4668
|
+
path: entity.path,
|
|
4669
|
+
matchType: "alias"
|
|
4670
|
+
}
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
for (const alias of aliases) {
|
|
4674
|
+
const existingByName = getEntityByName(moduleStateDb4, alias);
|
|
4675
|
+
if (existingByName && existingByName.name.toLowerCase() !== noteName.toLowerCase()) {
|
|
4676
|
+
collisions.push({
|
|
4677
|
+
term: alias,
|
|
4678
|
+
source: "alias",
|
|
4679
|
+
collidedWith: {
|
|
4680
|
+
name: existingByName.name,
|
|
4681
|
+
path: existingByName.path,
|
|
4682
|
+
matchType: "name"
|
|
4683
|
+
}
|
|
4684
|
+
});
|
|
4685
|
+
}
|
|
4686
|
+
const existingByAlias = getEntitiesByAlias(moduleStateDb4, alias);
|
|
4687
|
+
for (const entity of existingByAlias) {
|
|
4688
|
+
if (entity.name.toLowerCase() === noteName.toLowerCase()) continue;
|
|
4689
|
+
if (existingByName && existingByName.name.toLowerCase() === entity.name.toLowerCase()) continue;
|
|
4690
|
+
collisions.push({
|
|
4691
|
+
term: alias,
|
|
4692
|
+
source: "alias",
|
|
4693
|
+
collidedWith: {
|
|
4694
|
+
name: entity.name,
|
|
4695
|
+
path: entity.path,
|
|
4696
|
+
matchType: "alias"
|
|
4697
|
+
}
|
|
4698
|
+
});
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
return collisions;
|
|
4702
|
+
}
|
|
4703
|
+
function suggestAliases(noteName, existingAliases = [], category) {
|
|
4704
|
+
const suggestions = [];
|
|
4705
|
+
const existingLower = new Set(existingAliases.map((a) => a.toLowerCase()));
|
|
4706
|
+
const words = noteName.split(/\s+/).filter((w) => w.length > 0);
|
|
4707
|
+
function isSafe(alias) {
|
|
4708
|
+
if (existingLower.has(alias.toLowerCase())) return false;
|
|
4709
|
+
if (alias.toLowerCase() === noteName.toLowerCase()) return false;
|
|
4710
|
+
if (!moduleStateDb4) return true;
|
|
4711
|
+
const existing = getEntityByName(moduleStateDb4, alias);
|
|
4712
|
+
return !existing;
|
|
4713
|
+
}
|
|
4714
|
+
const inferredCategory = category || inferCategoryFromName(noteName);
|
|
4715
|
+
if (inferredCategory === "people" && words.length >= 2) {
|
|
4716
|
+
const firstName = words[0];
|
|
4717
|
+
const lastName = words[words.length - 1];
|
|
4718
|
+
if (firstName.length >= 2 && isSafe(firstName)) {
|
|
4719
|
+
suggestions.push({ alias: firstName, reason: "First name for quick reference" });
|
|
4720
|
+
}
|
|
4721
|
+
if (lastName.length >= 2 && lastName !== firstName && isSafe(lastName)) {
|
|
4722
|
+
suggestions.push({ alias: lastName, reason: "Last name for quick reference" });
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
if (words.length >= 3) {
|
|
4726
|
+
const acronym = words.map((w) => w[0]).join("").toUpperCase();
|
|
4727
|
+
if (acronym.length >= 3 && isSafe(acronym)) {
|
|
4728
|
+
suggestions.push({ alias: acronym, reason: `Acronym for "${noteName}"` });
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
if (noteName.includes("-")) {
|
|
4732
|
+
const unhyphenated = noteName.replace(/-/g, "");
|
|
4733
|
+
if (unhyphenated !== noteName && isSafe(unhyphenated)) {
|
|
4734
|
+
suggestions.push({ alias: unhyphenated, reason: "Unhyphenated form" });
|
|
4735
|
+
}
|
|
4736
|
+
const spaced = noteName.replace(/-/g, " ");
|
|
4737
|
+
if (spaced !== noteName && isSafe(spaced)) {
|
|
4738
|
+
suggestions.push({ alias: spaced, reason: "Space-separated form" });
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
return suggestions;
|
|
4742
|
+
}
|
|
4743
|
+
function inferCategoryFromName(name) {
|
|
4744
|
+
const words = name.split(/\s+/);
|
|
4745
|
+
if (words.length === 2 || words.length === 3) {
|
|
4746
|
+
const allCapitalized = words.every((w) => /^[A-Z][a-z]/.test(w));
|
|
4747
|
+
if (allCapitalized) return "people";
|
|
4748
|
+
}
|
|
4749
|
+
return void 0;
|
|
4750
|
+
}
|
|
4751
|
+
function checkPreflightSimilarity(noteName) {
|
|
4752
|
+
const result = { similarEntities: [] };
|
|
4753
|
+
if (!moduleStateDb4) return result;
|
|
4754
|
+
const exact = getEntityByName(moduleStateDb4, noteName);
|
|
4755
|
+
if (exact) {
|
|
4756
|
+
result.existingEntity = {
|
|
4757
|
+
name: exact.name,
|
|
4758
|
+
path: exact.path,
|
|
4759
|
+
category: exact.category
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4762
|
+
try {
|
|
4763
|
+
const searchResults = searchEntitiesDb(moduleStateDb4, noteName, 5);
|
|
4764
|
+
for (const sr of searchResults) {
|
|
4765
|
+
if (sr.name.toLowerCase() === noteName.toLowerCase()) continue;
|
|
4766
|
+
result.similarEntities.push({
|
|
4767
|
+
name: sr.name,
|
|
4768
|
+
path: sr.path,
|
|
4769
|
+
category: sr.category,
|
|
4770
|
+
rank: sr.rank
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
4773
|
+
} catch {
|
|
4774
|
+
}
|
|
4775
|
+
return result;
|
|
4776
|
+
}
|
|
4653
4777
|
|
|
4654
4778
|
// src/core/write/logging.ts
|
|
4655
4779
|
import {
|
|
@@ -4672,11 +4796,153 @@ async function flushLogs() {
|
|
|
4672
4796
|
if (logger2) await logger2.flush();
|
|
4673
4797
|
}
|
|
4674
4798
|
|
|
4799
|
+
// src/core/read/fts5.ts
|
|
4800
|
+
import * as fs5 from "fs";
|
|
4801
|
+
var EXCLUDED_DIRS2 = /* @__PURE__ */ new Set([
|
|
4802
|
+
".obsidian",
|
|
4803
|
+
".trash",
|
|
4804
|
+
".git",
|
|
4805
|
+
"node_modules",
|
|
4806
|
+
"templates",
|
|
4807
|
+
".claude",
|
|
4808
|
+
".flywheel"
|
|
4809
|
+
]);
|
|
4810
|
+
var MAX_INDEX_FILE_SIZE = 5 * 1024 * 1024;
|
|
4811
|
+
var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
|
|
4812
|
+
var db = null;
|
|
4813
|
+
var state = {
|
|
4814
|
+
ready: false,
|
|
4815
|
+
lastBuilt: null,
|
|
4816
|
+
noteCount: 0,
|
|
4817
|
+
error: null
|
|
4818
|
+
};
|
|
4819
|
+
function setFTS5Database(database) {
|
|
4820
|
+
db = database;
|
|
4821
|
+
try {
|
|
4822
|
+
const row = db.prepare(
|
|
4823
|
+
"SELECT value FROM fts_metadata WHERE key = ?"
|
|
4824
|
+
).get("last_built");
|
|
4825
|
+
if (row) {
|
|
4826
|
+
const lastBuilt = new Date(row.value);
|
|
4827
|
+
const countRow = db.prepare("SELECT COUNT(*) as count FROM notes_fts").get();
|
|
4828
|
+
state = {
|
|
4829
|
+
ready: countRow.count > 0,
|
|
4830
|
+
lastBuilt,
|
|
4831
|
+
noteCount: countRow.count,
|
|
4832
|
+
error: null
|
|
4833
|
+
};
|
|
4834
|
+
}
|
|
4835
|
+
} catch {
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
function shouldIndexFile(filePath) {
|
|
4839
|
+
const parts = filePath.split("/");
|
|
4840
|
+
return !parts.some((part) => EXCLUDED_DIRS2.has(part));
|
|
4841
|
+
}
|
|
4842
|
+
async function buildFTS5Index(vaultPath2) {
|
|
4843
|
+
try {
|
|
4844
|
+
state.error = null;
|
|
4845
|
+
if (!db) {
|
|
4846
|
+
throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
|
|
4847
|
+
}
|
|
4848
|
+
db.exec("DELETE FROM notes_fts");
|
|
4849
|
+
const files = await scanVault(vaultPath2);
|
|
4850
|
+
const indexableFiles = files.filter((f) => shouldIndexFile(f.path));
|
|
4851
|
+
const insert = db.prepare(
|
|
4852
|
+
"INSERT INTO notes_fts (path, title, content) VALUES (?, ?, ?)"
|
|
4853
|
+
);
|
|
4854
|
+
const insertMany = db.transaction((filesToIndex) => {
|
|
4855
|
+
let indexed2 = 0;
|
|
4856
|
+
for (const file of filesToIndex) {
|
|
4857
|
+
try {
|
|
4858
|
+
const stats = fs5.statSync(file.absolutePath);
|
|
4859
|
+
if (stats.size > MAX_INDEX_FILE_SIZE) {
|
|
4860
|
+
continue;
|
|
4861
|
+
}
|
|
4862
|
+
const content = fs5.readFileSync(file.absolutePath, "utf-8");
|
|
4863
|
+
const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
|
|
4864
|
+
insert.run(file.path, title, content);
|
|
4865
|
+
indexed2++;
|
|
4866
|
+
} catch (err) {
|
|
4867
|
+
console.error(`[FTS5] Skipping ${file.path}:`, err);
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
return indexed2;
|
|
4871
|
+
});
|
|
4872
|
+
const indexed = insertMany(indexableFiles);
|
|
4873
|
+
const now = /* @__PURE__ */ new Date();
|
|
4874
|
+
db.prepare(
|
|
4875
|
+
"INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
|
|
4876
|
+
).run("last_built", now.toISOString());
|
|
4877
|
+
state = {
|
|
4878
|
+
ready: true,
|
|
4879
|
+
lastBuilt: now,
|
|
4880
|
+
noteCount: indexed,
|
|
4881
|
+
error: null
|
|
4882
|
+
};
|
|
4883
|
+
console.error(`[FTS5] Indexed ${indexed} notes`);
|
|
4884
|
+
return state;
|
|
4885
|
+
} catch (err) {
|
|
4886
|
+
state = {
|
|
4887
|
+
ready: false,
|
|
4888
|
+
lastBuilt: null,
|
|
4889
|
+
noteCount: 0,
|
|
4890
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4891
|
+
};
|
|
4892
|
+
throw err;
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
function isIndexStale(_vaultPath) {
|
|
4896
|
+
if (!db) {
|
|
4897
|
+
return true;
|
|
4898
|
+
}
|
|
4899
|
+
try {
|
|
4900
|
+
const row = db.prepare(
|
|
4901
|
+
"SELECT value FROM fts_metadata WHERE key = ?"
|
|
4902
|
+
).get("last_built");
|
|
4903
|
+
if (!row) {
|
|
4904
|
+
return true;
|
|
4905
|
+
}
|
|
4906
|
+
const lastBuilt = new Date(row.value);
|
|
4907
|
+
const age = Date.now() - lastBuilt.getTime();
|
|
4908
|
+
return age > STALE_THRESHOLD_MS;
|
|
4909
|
+
} catch {
|
|
4910
|
+
return true;
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
function searchFTS5(_vaultPath, query, limit = 10) {
|
|
4914
|
+
if (!db) {
|
|
4915
|
+
throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
|
|
4916
|
+
}
|
|
4917
|
+
try {
|
|
4918
|
+
const stmt = db.prepare(`
|
|
4919
|
+
SELECT
|
|
4920
|
+
path,
|
|
4921
|
+
title,
|
|
4922
|
+
snippet(notes_fts, 2, '[', ']', '...', 20) as snippet
|
|
4923
|
+
FROM notes_fts
|
|
4924
|
+
WHERE notes_fts MATCH ?
|
|
4925
|
+
ORDER BY rank
|
|
4926
|
+
LIMIT ?
|
|
4927
|
+
`);
|
|
4928
|
+
const results = stmt.all(query, limit);
|
|
4929
|
+
return results;
|
|
4930
|
+
} catch (err) {
|
|
4931
|
+
if (err instanceof Error && err.message.includes("fts5: syntax error")) {
|
|
4932
|
+
throw new Error(`Invalid search query: ${query}. Check FTS5 syntax.`);
|
|
4933
|
+
}
|
|
4934
|
+
throw err;
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
function getFTS5State() {
|
|
4938
|
+
return { ...state };
|
|
4939
|
+
}
|
|
4940
|
+
|
|
4675
4941
|
// src/index.ts
|
|
4676
4942
|
import { openStateDb, scanVaultEntities as scanVaultEntities3 } from "@velvetmonkey/vault-core";
|
|
4677
4943
|
|
|
4678
4944
|
// src/tools/read/graph.ts
|
|
4679
|
-
import * as
|
|
4945
|
+
import * as fs6 from "fs";
|
|
4680
4946
|
import * as path8 from "path";
|
|
4681
4947
|
import { z } from "zod";
|
|
4682
4948
|
|
|
@@ -4701,7 +4967,7 @@ function requireIndex() {
|
|
|
4701
4967
|
async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
|
|
4702
4968
|
try {
|
|
4703
4969
|
const fullPath = path8.join(vaultPath2, sourcePath);
|
|
4704
|
-
const content = await
|
|
4970
|
+
const content = await fs6.promises.readFile(fullPath, "utf-8");
|
|
4705
4971
|
const lines = content.split("\n");
|
|
4706
4972
|
const startLine = Math.max(0, line - 1 - contextLines);
|
|
4707
4973
|
const endLine = Math.min(lines.length, line + contextLines);
|
|
@@ -5091,14 +5357,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
5091
5357
|
};
|
|
5092
5358
|
function findSimilarEntity2(target, entities) {
|
|
5093
5359
|
const targetLower = target.toLowerCase();
|
|
5094
|
-
for (const [name,
|
|
5360
|
+
for (const [name, path24] of entities) {
|
|
5095
5361
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
5096
|
-
return
|
|
5362
|
+
return path24;
|
|
5097
5363
|
}
|
|
5098
5364
|
}
|
|
5099
|
-
for (const [name,
|
|
5365
|
+
for (const [name, path24] of entities) {
|
|
5100
5366
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
5101
|
-
return
|
|
5367
|
+
return path24;
|
|
5102
5368
|
}
|
|
5103
5369
|
}
|
|
5104
5370
|
return void 0;
|
|
@@ -5178,7 +5444,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
5178
5444
|
}
|
|
5179
5445
|
|
|
5180
5446
|
// src/tools/read/health.ts
|
|
5181
|
-
import * as
|
|
5447
|
+
import * as fs7 from "fs";
|
|
5182
5448
|
import { z as z3 } from "zod";
|
|
5183
5449
|
var STALE_THRESHOLD_SECONDS = 300;
|
|
5184
5450
|
function registerHealthTools(server2, getIndex, getVaultPath) {
|
|
@@ -5218,7 +5484,7 @@ function registerHealthTools(server2, getIndex, getVaultPath) {
|
|
|
5218
5484
|
const indexErrorObj = getIndexError();
|
|
5219
5485
|
let vaultAccessible = false;
|
|
5220
5486
|
try {
|
|
5221
|
-
|
|
5487
|
+
fs7.accessSync(vaultPath2, fs7.constants.R_OK);
|
|
5222
5488
|
vaultAccessible = true;
|
|
5223
5489
|
} catch {
|
|
5224
5490
|
vaultAccessible = false;
|
|
@@ -5395,8 +5661,8 @@ function registerHealthTools(server2, getIndex, getVaultPath) {
|
|
|
5395
5661
|
top_tags: z3.array(TagStatSchema).describe("Top 20 most used tags"),
|
|
5396
5662
|
folders: z3.array(FolderStatSchema).describe("Note counts by top-level folder")
|
|
5397
5663
|
};
|
|
5398
|
-
function isPeriodicNote(
|
|
5399
|
-
const filename =
|
|
5664
|
+
function isPeriodicNote(path24) {
|
|
5665
|
+
const filename = path24.split("/").pop() || "";
|
|
5400
5666
|
const nameWithoutExt = filename.replace(/\.md$/, "");
|
|
5401
5667
|
const patterns = [
|
|
5402
5668
|
/^\d{4}-\d{2}-\d{2}$/,
|
|
@@ -5411,7 +5677,7 @@ function registerHealthTools(server2, getIndex, getVaultPath) {
|
|
|
5411
5677
|
// YYYY (yearly)
|
|
5412
5678
|
];
|
|
5413
5679
|
const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
|
|
5414
|
-
const folder =
|
|
5680
|
+
const folder = path24.split("/")[0]?.toLowerCase() || "";
|
|
5415
5681
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
5416
5682
|
}
|
|
5417
5683
|
server2.registerTool(
|
|
@@ -5504,166 +5770,6 @@ function registerHealthTools(server2, getIndex, getVaultPath) {
|
|
|
5504
5770
|
|
|
5505
5771
|
// src/tools/read/query.ts
|
|
5506
5772
|
import { z as z4 } from "zod";
|
|
5507
|
-
|
|
5508
|
-
// src/core/read/fts5.ts
|
|
5509
|
-
import Database from "better-sqlite3";
|
|
5510
|
-
import * as fs7 from "fs";
|
|
5511
|
-
import * as path9 from "path";
|
|
5512
|
-
var EXCLUDED_DIRS2 = /* @__PURE__ */ new Set([
|
|
5513
|
-
".obsidian",
|
|
5514
|
-
".trash",
|
|
5515
|
-
".git",
|
|
5516
|
-
"node_modules",
|
|
5517
|
-
"templates",
|
|
5518
|
-
".claude"
|
|
5519
|
-
]);
|
|
5520
|
-
var MAX_INDEX_FILE_SIZE = 5 * 1024 * 1024;
|
|
5521
|
-
var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
|
|
5522
|
-
var db = null;
|
|
5523
|
-
var state = {
|
|
5524
|
-
ready: false,
|
|
5525
|
-
lastBuilt: null,
|
|
5526
|
-
noteCount: 0,
|
|
5527
|
-
error: null
|
|
5528
|
-
};
|
|
5529
|
-
function getDbPath(vaultPath2) {
|
|
5530
|
-
const claudeDir = path9.join(vaultPath2, ".claude");
|
|
5531
|
-
if (!fs7.existsSync(claudeDir)) {
|
|
5532
|
-
fs7.mkdirSync(claudeDir, { recursive: true });
|
|
5533
|
-
}
|
|
5534
|
-
return path9.join(claudeDir, "vault-search.db");
|
|
5535
|
-
}
|
|
5536
|
-
function initDatabase(vaultPath2) {
|
|
5537
|
-
const dbPath = getDbPath(vaultPath2);
|
|
5538
|
-
const database = new Database(dbPath);
|
|
5539
|
-
database.exec(`
|
|
5540
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
|
|
5541
|
-
path,
|
|
5542
|
-
title,
|
|
5543
|
-
content,
|
|
5544
|
-
tokenize='porter'
|
|
5545
|
-
);
|
|
5546
|
-
|
|
5547
|
-
CREATE TABLE IF NOT EXISTS fts_metadata (
|
|
5548
|
-
key TEXT PRIMARY KEY,
|
|
5549
|
-
value TEXT
|
|
5550
|
-
);
|
|
5551
|
-
`);
|
|
5552
|
-
return database;
|
|
5553
|
-
}
|
|
5554
|
-
function shouldIndexFile(filePath) {
|
|
5555
|
-
const parts = filePath.split("/");
|
|
5556
|
-
return !parts.some((part) => EXCLUDED_DIRS2.has(part));
|
|
5557
|
-
}
|
|
5558
|
-
async function buildFTS5Index(vaultPath2) {
|
|
5559
|
-
try {
|
|
5560
|
-
state.error = null;
|
|
5561
|
-
db = initDatabase(vaultPath2);
|
|
5562
|
-
db.exec("DELETE FROM notes_fts");
|
|
5563
|
-
const files = await scanVault(vaultPath2);
|
|
5564
|
-
const indexableFiles = files.filter((f) => shouldIndexFile(f.path));
|
|
5565
|
-
const insert = db.prepare(
|
|
5566
|
-
"INSERT INTO notes_fts (path, title, content) VALUES (?, ?, ?)"
|
|
5567
|
-
);
|
|
5568
|
-
const insertMany = db.transaction((filesToIndex) => {
|
|
5569
|
-
let indexed2 = 0;
|
|
5570
|
-
for (const file of filesToIndex) {
|
|
5571
|
-
try {
|
|
5572
|
-
const stats = fs7.statSync(file.absolutePath);
|
|
5573
|
-
if (stats.size > MAX_INDEX_FILE_SIZE) {
|
|
5574
|
-
continue;
|
|
5575
|
-
}
|
|
5576
|
-
const content = fs7.readFileSync(file.absolutePath, "utf-8");
|
|
5577
|
-
const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
|
|
5578
|
-
insert.run(file.path, title, content);
|
|
5579
|
-
indexed2++;
|
|
5580
|
-
} catch (err) {
|
|
5581
|
-
console.error(`[FTS5] Skipping ${file.path}:`, err);
|
|
5582
|
-
}
|
|
5583
|
-
}
|
|
5584
|
-
return indexed2;
|
|
5585
|
-
});
|
|
5586
|
-
const indexed = insertMany(indexableFiles);
|
|
5587
|
-
const now = /* @__PURE__ */ new Date();
|
|
5588
|
-
db.prepare(
|
|
5589
|
-
"INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
|
|
5590
|
-
).run("last_built", now.toISOString());
|
|
5591
|
-
state = {
|
|
5592
|
-
ready: true,
|
|
5593
|
-
lastBuilt: now,
|
|
5594
|
-
noteCount: indexed,
|
|
5595
|
-
error: null
|
|
5596
|
-
};
|
|
5597
|
-
console.error(`[FTS5] Indexed ${indexed} notes`);
|
|
5598
|
-
return state;
|
|
5599
|
-
} catch (err) {
|
|
5600
|
-
state = {
|
|
5601
|
-
ready: false,
|
|
5602
|
-
lastBuilt: null,
|
|
5603
|
-
noteCount: 0,
|
|
5604
|
-
error: err instanceof Error ? err.message : String(err)
|
|
5605
|
-
};
|
|
5606
|
-
throw err;
|
|
5607
|
-
}
|
|
5608
|
-
}
|
|
5609
|
-
function isIndexStale(vaultPath2) {
|
|
5610
|
-
const dbPath = getDbPath(vaultPath2);
|
|
5611
|
-
if (!fs7.existsSync(dbPath)) {
|
|
5612
|
-
return true;
|
|
5613
|
-
}
|
|
5614
|
-
try {
|
|
5615
|
-
const database = new Database(dbPath, { readonly: true });
|
|
5616
|
-
const row = database.prepare(
|
|
5617
|
-
"SELECT value FROM fts_metadata WHERE key = ?"
|
|
5618
|
-
).get("last_built");
|
|
5619
|
-
database.close();
|
|
5620
|
-
if (!row) {
|
|
5621
|
-
return true;
|
|
5622
|
-
}
|
|
5623
|
-
const lastBuilt = new Date(row.value);
|
|
5624
|
-
const age = Date.now() - lastBuilt.getTime();
|
|
5625
|
-
return age > STALE_THRESHOLD_MS;
|
|
5626
|
-
} catch {
|
|
5627
|
-
return true;
|
|
5628
|
-
}
|
|
5629
|
-
}
|
|
5630
|
-
function ensureDb(vaultPath2) {
|
|
5631
|
-
if (!db) {
|
|
5632
|
-
const dbPath = getDbPath(vaultPath2);
|
|
5633
|
-
if (!fs7.existsSync(dbPath)) {
|
|
5634
|
-
throw new Error("Search index not built. Call rebuild_search_index first.");
|
|
5635
|
-
}
|
|
5636
|
-
db = new Database(dbPath);
|
|
5637
|
-
}
|
|
5638
|
-
return db;
|
|
5639
|
-
}
|
|
5640
|
-
function searchFTS5(vaultPath2, query, limit = 10) {
|
|
5641
|
-
const database = ensureDb(vaultPath2);
|
|
5642
|
-
try {
|
|
5643
|
-
const stmt = database.prepare(`
|
|
5644
|
-
SELECT
|
|
5645
|
-
path,
|
|
5646
|
-
title,
|
|
5647
|
-
snippet(notes_fts, 2, '[', ']', '...', 20) as snippet
|
|
5648
|
-
FROM notes_fts
|
|
5649
|
-
WHERE notes_fts MATCH ?
|
|
5650
|
-
ORDER BY rank
|
|
5651
|
-
LIMIT ?
|
|
5652
|
-
`);
|
|
5653
|
-
const results = stmt.all(query, limit);
|
|
5654
|
-
return results;
|
|
5655
|
-
} catch (err) {
|
|
5656
|
-
if (err instanceof Error && err.message.includes("fts5: syntax error")) {
|
|
5657
|
-
throw new Error(`Invalid search query: ${query}. Check FTS5 syntax.`);
|
|
5658
|
-
}
|
|
5659
|
-
throw err;
|
|
5660
|
-
}
|
|
5661
|
-
}
|
|
5662
|
-
function getFTS5State() {
|
|
5663
|
-
return { ...state };
|
|
5664
|
-
}
|
|
5665
|
-
|
|
5666
|
-
// src/tools/read/query.ts
|
|
5667
5773
|
import {
|
|
5668
5774
|
searchEntities,
|
|
5669
5775
|
searchEntitiesPrefix
|
|
@@ -6033,7 +6139,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
6033
6139
|
|
|
6034
6140
|
// src/tools/read/system.ts
|
|
6035
6141
|
import * as fs8 from "fs";
|
|
6036
|
-
import * as
|
|
6142
|
+
import * as path9 from "path";
|
|
6037
6143
|
import { z as z5 } from "zod";
|
|
6038
6144
|
import { scanVaultEntities as scanVaultEntities2 } from "@velvetmonkey/vault-core";
|
|
6039
6145
|
function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
|
|
@@ -6311,7 +6417,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
6311
6417
|
continue;
|
|
6312
6418
|
}
|
|
6313
6419
|
try {
|
|
6314
|
-
const fullPath =
|
|
6420
|
+
const fullPath = path9.join(vaultPath2, note.path);
|
|
6315
6421
|
const content = await fs8.promises.readFile(fullPath, "utf-8");
|
|
6316
6422
|
const lines = content.split("\n");
|
|
6317
6423
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -6427,7 +6533,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
6427
6533
|
let wordCount;
|
|
6428
6534
|
if (include_word_count) {
|
|
6429
6535
|
try {
|
|
6430
|
-
const fullPath =
|
|
6536
|
+
const fullPath = path9.join(vaultPath2, resolvedPath);
|
|
6431
6537
|
const content = await fs8.promises.readFile(fullPath, "utf-8");
|
|
6432
6538
|
wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
6433
6539
|
} catch {
|
|
@@ -6593,8 +6699,8 @@ function getStaleNotes(index, days, minBacklinks = 0) {
|
|
|
6593
6699
|
return b.days_since_modified - a.days_since_modified;
|
|
6594
6700
|
});
|
|
6595
6701
|
}
|
|
6596
|
-
function getContemporaneousNotes(index,
|
|
6597
|
-
const targetNote = index.notes.get(
|
|
6702
|
+
function getContemporaneousNotes(index, path24, hours = 24) {
|
|
6703
|
+
const targetNote = index.notes.get(path24);
|
|
6598
6704
|
if (!targetNote) {
|
|
6599
6705
|
return [];
|
|
6600
6706
|
}
|
|
@@ -6602,7 +6708,7 @@ function getContemporaneousNotes(index, path25, hours = 24) {
|
|
|
6602
6708
|
const windowMs = hours * 60 * 60 * 1e3;
|
|
6603
6709
|
const results = [];
|
|
6604
6710
|
for (const note of index.notes.values()) {
|
|
6605
|
-
if (note.path ===
|
|
6711
|
+
if (note.path === path24) continue;
|
|
6606
6712
|
const timeDiff = Math.abs(note.modified.getTime() - targetTime);
|
|
6607
6713
|
if (timeDiff <= windowMs) {
|
|
6608
6714
|
results.push({
|
|
@@ -6651,7 +6757,7 @@ function getActivitySummary(index, days) {
|
|
|
6651
6757
|
|
|
6652
6758
|
// src/tools/read/structure.ts
|
|
6653
6759
|
import * as fs9 from "fs";
|
|
6654
|
-
import * as
|
|
6760
|
+
import * as path10 from "path";
|
|
6655
6761
|
var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
|
|
6656
6762
|
function extractHeadings(content) {
|
|
6657
6763
|
const lines = content.split("\n");
|
|
@@ -6705,7 +6811,7 @@ function buildSections(headings, totalLines) {
|
|
|
6705
6811
|
async function getNoteStructure(index, notePath, vaultPath2) {
|
|
6706
6812
|
const note = index.notes.get(notePath);
|
|
6707
6813
|
if (!note) return null;
|
|
6708
|
-
const absolutePath =
|
|
6814
|
+
const absolutePath = path10.join(vaultPath2, notePath);
|
|
6709
6815
|
let content;
|
|
6710
6816
|
try {
|
|
6711
6817
|
content = await fs9.promises.readFile(absolutePath, "utf-8");
|
|
@@ -6728,7 +6834,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
|
|
|
6728
6834
|
async function getHeadings(index, notePath, vaultPath2) {
|
|
6729
6835
|
const note = index.notes.get(notePath);
|
|
6730
6836
|
if (!note) return null;
|
|
6731
|
-
const absolutePath =
|
|
6837
|
+
const absolutePath = path10.join(vaultPath2, notePath);
|
|
6732
6838
|
let content;
|
|
6733
6839
|
try {
|
|
6734
6840
|
content = await fs9.promises.readFile(absolutePath, "utf-8");
|
|
@@ -6740,7 +6846,7 @@ async function getHeadings(index, notePath, vaultPath2) {
|
|
|
6740
6846
|
async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
|
|
6741
6847
|
const note = index.notes.get(notePath);
|
|
6742
6848
|
if (!note) return null;
|
|
6743
|
-
const absolutePath =
|
|
6849
|
+
const absolutePath = path10.join(vaultPath2, notePath);
|
|
6744
6850
|
let content;
|
|
6745
6851
|
try {
|
|
6746
6852
|
content = await fs9.promises.readFile(absolutePath, "utf-8");
|
|
@@ -6782,7 +6888,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
6782
6888
|
const results = [];
|
|
6783
6889
|
for (const note of index.notes.values()) {
|
|
6784
6890
|
if (folder && !note.path.startsWith(folder)) continue;
|
|
6785
|
-
const absolutePath =
|
|
6891
|
+
const absolutePath = path10.join(vaultPath2, note.path);
|
|
6786
6892
|
let content;
|
|
6787
6893
|
try {
|
|
6788
6894
|
content = await fs9.promises.readFile(absolutePath, "utf-8");
|
|
@@ -6806,7 +6912,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
6806
6912
|
|
|
6807
6913
|
// src/tools/read/tasks.ts
|
|
6808
6914
|
import * as fs10 from "fs";
|
|
6809
|
-
import * as
|
|
6915
|
+
import * as path11 from "path";
|
|
6810
6916
|
var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
|
|
6811
6917
|
var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
|
|
6812
6918
|
var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
|
|
@@ -6875,7 +6981,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
|
|
|
6875
6981
|
const allTasks = [];
|
|
6876
6982
|
for (const note of index.notes.values()) {
|
|
6877
6983
|
if (folder && !note.path.startsWith(folder)) continue;
|
|
6878
|
-
const absolutePath =
|
|
6984
|
+
const absolutePath = path11.join(vaultPath2, note.path);
|
|
6879
6985
|
const tasks = await extractTasksFromNote(note.path, absolutePath);
|
|
6880
6986
|
allTasks.push(...tasks);
|
|
6881
6987
|
}
|
|
@@ -6906,7 +7012,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
|
|
|
6906
7012
|
async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
|
|
6907
7013
|
const note = index.notes.get(notePath);
|
|
6908
7014
|
if (!note) return null;
|
|
6909
|
-
const absolutePath =
|
|
7015
|
+
const absolutePath = path11.join(vaultPath2, notePath);
|
|
6910
7016
|
let tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6911
7017
|
if (excludeTags.length > 0) {
|
|
6912
7018
|
tasks = tasks.filter(
|
|
@@ -7394,14 +7500,14 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7394
7500
|
offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
7395
7501
|
}
|
|
7396
7502
|
},
|
|
7397
|
-
async ({ path:
|
|
7503
|
+
async ({ path: path24, hours, limit: requestedLimit, offset }) => {
|
|
7398
7504
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
7399
7505
|
const index = getIndex();
|
|
7400
|
-
const allResults = getContemporaneousNotes(index,
|
|
7506
|
+
const allResults = getContemporaneousNotes(index, path24, hours);
|
|
7401
7507
|
const result = allResults.slice(offset, offset + limit);
|
|
7402
7508
|
return {
|
|
7403
7509
|
content: [{ type: "text", text: JSON.stringify({
|
|
7404
|
-
reference_note:
|
|
7510
|
+
reference_note: path24,
|
|
7405
7511
|
window_hours: hours,
|
|
7406
7512
|
total_count: allResults.length,
|
|
7407
7513
|
returned_count: result.length,
|
|
@@ -7439,13 +7545,13 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7439
7545
|
path: z6.string().describe("Path to the note")
|
|
7440
7546
|
}
|
|
7441
7547
|
},
|
|
7442
|
-
async ({ path:
|
|
7548
|
+
async ({ path: path24 }) => {
|
|
7443
7549
|
const index = getIndex();
|
|
7444
7550
|
const vaultPath2 = getVaultPath();
|
|
7445
|
-
const result = await getNoteStructure(index,
|
|
7551
|
+
const result = await getNoteStructure(index, path24, vaultPath2);
|
|
7446
7552
|
if (!result) {
|
|
7447
7553
|
return {
|
|
7448
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
7554
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path24 }, null, 2) }]
|
|
7449
7555
|
};
|
|
7450
7556
|
}
|
|
7451
7557
|
return {
|
|
@@ -7462,18 +7568,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7462
7568
|
path: z6.string().describe("Path to the note")
|
|
7463
7569
|
}
|
|
7464
7570
|
},
|
|
7465
|
-
async ({ path:
|
|
7571
|
+
async ({ path: path24 }) => {
|
|
7466
7572
|
const index = getIndex();
|
|
7467
7573
|
const vaultPath2 = getVaultPath();
|
|
7468
|
-
const result = await getHeadings(index,
|
|
7574
|
+
const result = await getHeadings(index, path24, vaultPath2);
|
|
7469
7575
|
if (!result) {
|
|
7470
7576
|
return {
|
|
7471
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
7577
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path24 }, null, 2) }]
|
|
7472
7578
|
};
|
|
7473
7579
|
}
|
|
7474
7580
|
return {
|
|
7475
7581
|
content: [{ type: "text", text: JSON.stringify({
|
|
7476
|
-
path:
|
|
7582
|
+
path: path24,
|
|
7477
7583
|
heading_count: result.length,
|
|
7478
7584
|
headings: result
|
|
7479
7585
|
}, null, 2) }]
|
|
@@ -7491,15 +7597,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7491
7597
|
include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
|
|
7492
7598
|
}
|
|
7493
7599
|
},
|
|
7494
|
-
async ({ path:
|
|
7600
|
+
async ({ path: path24, heading, include_subheadings }) => {
|
|
7495
7601
|
const index = getIndex();
|
|
7496
7602
|
const vaultPath2 = getVaultPath();
|
|
7497
|
-
const result = await getSectionContent(index,
|
|
7603
|
+
const result = await getSectionContent(index, path24, heading, vaultPath2, include_subheadings);
|
|
7498
7604
|
if (!result) {
|
|
7499
7605
|
return {
|
|
7500
7606
|
content: [{ type: "text", text: JSON.stringify({
|
|
7501
7607
|
error: "Section not found",
|
|
7502
|
-
path:
|
|
7608
|
+
path: path24,
|
|
7503
7609
|
heading
|
|
7504
7610
|
}, null, 2) }]
|
|
7505
7611
|
};
|
|
@@ -7576,19 +7682,19 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7576
7682
|
path: z6.string().describe("Path to the note")
|
|
7577
7683
|
}
|
|
7578
7684
|
},
|
|
7579
|
-
async ({ path:
|
|
7685
|
+
async ({ path: path24 }) => {
|
|
7580
7686
|
const index = getIndex();
|
|
7581
7687
|
const vaultPath2 = getVaultPath();
|
|
7582
7688
|
const config = getConfig();
|
|
7583
|
-
const result = await getTasksFromNote(index,
|
|
7689
|
+
const result = await getTasksFromNote(index, path24, vaultPath2, config.exclude_task_tags || []);
|
|
7584
7690
|
if (!result) {
|
|
7585
7691
|
return {
|
|
7586
|
-
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path:
|
|
7692
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path24 }, null, 2) }]
|
|
7587
7693
|
};
|
|
7588
7694
|
}
|
|
7589
7695
|
return {
|
|
7590
7696
|
content: [{ type: "text", text: JSON.stringify({
|
|
7591
|
-
path:
|
|
7697
|
+
path: path24,
|
|
7592
7698
|
task_count: result.length,
|
|
7593
7699
|
open: result.filter((t) => t.status === "open").length,
|
|
7594
7700
|
completed: result.filter((t) => t.status === "completed").length,
|
|
@@ -7720,14 +7826,14 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
7720
7826
|
offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
|
|
7721
7827
|
}
|
|
7722
7828
|
},
|
|
7723
|
-
async ({ path:
|
|
7829
|
+
async ({ path: path24, limit: requestedLimit, offset }) => {
|
|
7724
7830
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
7725
7831
|
const index = getIndex();
|
|
7726
|
-
const allResults = findBidirectionalLinks(index,
|
|
7832
|
+
const allResults = findBidirectionalLinks(index, path24);
|
|
7727
7833
|
const result = allResults.slice(offset, offset + limit);
|
|
7728
7834
|
return {
|
|
7729
7835
|
content: [{ type: "text", text: JSON.stringify({
|
|
7730
|
-
scope:
|
|
7836
|
+
scope: path24 || "all",
|
|
7731
7837
|
total_count: allResults.length,
|
|
7732
7838
|
returned_count: result.length,
|
|
7733
7839
|
pairs: result
|
|
@@ -8135,13 +8241,13 @@ function registerPeriodicTools(server2, getIndex) {
|
|
|
8135
8241
|
// src/tools/read/bidirectional.ts
|
|
8136
8242
|
import { z as z8 } from "zod";
|
|
8137
8243
|
import * as fs11 from "fs/promises";
|
|
8138
|
-
import * as
|
|
8244
|
+
import * as path12 from "path";
|
|
8139
8245
|
import matter2 from "gray-matter";
|
|
8140
8246
|
var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
|
|
8141
8247
|
var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
|
|
8142
8248
|
var WIKILINK_REGEX2 = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]+)?\]\]/g;
|
|
8143
8249
|
async function readFileContent(notePath, vaultPath2) {
|
|
8144
|
-
const fullPath =
|
|
8250
|
+
const fullPath = path12.join(vaultPath2, notePath);
|
|
8145
8251
|
try {
|
|
8146
8252
|
return await fs11.readFile(fullPath, "utf-8");
|
|
8147
8253
|
} catch {
|
|
@@ -8877,10 +8983,10 @@ function registerSchemaTools(server2, getIndex, getVaultPath) {
|
|
|
8877
8983
|
// src/tools/read/computed.ts
|
|
8878
8984
|
import { z as z10 } from "zod";
|
|
8879
8985
|
import * as fs12 from "fs/promises";
|
|
8880
|
-
import * as
|
|
8986
|
+
import * as path13 from "path";
|
|
8881
8987
|
import matter3 from "gray-matter";
|
|
8882
8988
|
async function readFileContent2(notePath, vaultPath2) {
|
|
8883
|
-
const fullPath =
|
|
8989
|
+
const fullPath = path13.join(vaultPath2, notePath);
|
|
8884
8990
|
try {
|
|
8885
8991
|
return await fs12.readFile(fullPath, "utf-8");
|
|
8886
8992
|
} catch {
|
|
@@ -8888,7 +8994,7 @@ async function readFileContent2(notePath, vaultPath2) {
|
|
|
8888
8994
|
}
|
|
8889
8995
|
}
|
|
8890
8996
|
async function getFileStats(notePath, vaultPath2) {
|
|
8891
|
-
const fullPath =
|
|
8997
|
+
const fullPath = path13.join(vaultPath2, notePath);
|
|
8892
8998
|
try {
|
|
8893
8999
|
const stats = await fs12.stat(fullPath);
|
|
8894
9000
|
return {
|
|
@@ -9047,7 +9153,7 @@ function registerComputedTools(server2, getIndex, getVaultPath) {
|
|
|
9047
9153
|
// src/tools/read/migrations.ts
|
|
9048
9154
|
import { z as z11 } from "zod";
|
|
9049
9155
|
import * as fs13 from "fs/promises";
|
|
9050
|
-
import * as
|
|
9156
|
+
import * as path14 from "path";
|
|
9051
9157
|
import matter4 from "gray-matter";
|
|
9052
9158
|
function getNotesInFolder2(index, folder) {
|
|
9053
9159
|
const notes = [];
|
|
@@ -9060,7 +9166,7 @@ function getNotesInFolder2(index, folder) {
|
|
|
9060
9166
|
return notes;
|
|
9061
9167
|
}
|
|
9062
9168
|
async function readFileContent3(notePath, vaultPath2) {
|
|
9063
|
-
const fullPath =
|
|
9169
|
+
const fullPath = path14.join(vaultPath2, notePath);
|
|
9064
9170
|
try {
|
|
9065
9171
|
return await fs13.readFile(fullPath, "utf-8");
|
|
9066
9172
|
} catch {
|
|
@@ -9068,7 +9174,7 @@ async function readFileContent3(notePath, vaultPath2) {
|
|
|
9068
9174
|
}
|
|
9069
9175
|
}
|
|
9070
9176
|
async function writeFileContent(notePath, vaultPath2, content) {
|
|
9071
|
-
const fullPath =
|
|
9177
|
+
const fullPath = path14.join(vaultPath2, notePath);
|
|
9072
9178
|
try {
|
|
9073
9179
|
await fs13.writeFile(fullPath, content, "utf-8");
|
|
9074
9180
|
return true;
|
|
@@ -9452,7 +9558,7 @@ function runValidationPipeline(content, format, options = {}) {
|
|
|
9452
9558
|
// src/core/write/mutation-helpers.ts
|
|
9453
9559
|
init_writer();
|
|
9454
9560
|
import fs15 from "fs/promises";
|
|
9455
|
-
import
|
|
9561
|
+
import path16 from "path";
|
|
9456
9562
|
init_constants();
|
|
9457
9563
|
init_writer();
|
|
9458
9564
|
function formatMcpResult(result) {
|
|
@@ -9501,7 +9607,7 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
|
|
|
9501
9607
|
return info;
|
|
9502
9608
|
}
|
|
9503
9609
|
async function ensureFileExists(vaultPath2, notePath) {
|
|
9504
|
-
const fullPath =
|
|
9610
|
+
const fullPath = path16.join(vaultPath2, notePath);
|
|
9505
9611
|
try {
|
|
9506
9612
|
await fs15.access(fullPath);
|
|
9507
9613
|
return null;
|
|
@@ -10060,8 +10166,8 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
10060
10166
|
init_writer();
|
|
10061
10167
|
import { z as z15 } from "zod";
|
|
10062
10168
|
import fs16 from "fs/promises";
|
|
10063
|
-
import
|
|
10064
|
-
function registerNoteTools(server2, vaultPath2) {
|
|
10169
|
+
import path17 from "path";
|
|
10170
|
+
function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
10065
10171
|
server2.tool(
|
|
10066
10172
|
"vault_create_note",
|
|
10067
10173
|
"Create a new note in the vault with optional frontmatter and content",
|
|
@@ -10082,13 +10188,39 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
10082
10188
|
if (!validatePath(vaultPath2, notePath)) {
|
|
10083
10189
|
return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
|
|
10084
10190
|
}
|
|
10085
|
-
const fullPath =
|
|
10191
|
+
const fullPath = path17.join(vaultPath2, notePath);
|
|
10086
10192
|
const existsCheck = await ensureFileExists(vaultPath2, notePath);
|
|
10087
10193
|
if (existsCheck === null && !overwrite) {
|
|
10088
10194
|
return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
|
|
10089
10195
|
}
|
|
10090
|
-
const dir =
|
|
10196
|
+
const dir = path17.dirname(fullPath);
|
|
10091
10197
|
await fs16.mkdir(dir, { recursive: true });
|
|
10198
|
+
const warnings = [];
|
|
10199
|
+
const noteName = path17.basename(notePath, ".md");
|
|
10200
|
+
const existingAliases = Array.isArray(frontmatter?.aliases) ? frontmatter.aliases.filter((a) => typeof a === "string") : [];
|
|
10201
|
+
const preflight = checkPreflightSimilarity(noteName);
|
|
10202
|
+
if (preflight.existingEntity) {
|
|
10203
|
+
warnings.push({
|
|
10204
|
+
type: "similar_note_exists",
|
|
10205
|
+
message: `An entity "${preflight.existingEntity.name}" already exists at ${preflight.existingEntity.path}`,
|
|
10206
|
+
suggestion: `Consider linking to the existing note instead, or choose a different name`
|
|
10207
|
+
});
|
|
10208
|
+
}
|
|
10209
|
+
for (const similar of preflight.similarEntities.slice(0, 3)) {
|
|
10210
|
+
warnings.push({
|
|
10211
|
+
type: "similar_note_exists",
|
|
10212
|
+
message: `Similar entity "${similar.name}" exists at ${similar.path}`,
|
|
10213
|
+
suggestion: `Check if this is a duplicate`
|
|
10214
|
+
});
|
|
10215
|
+
}
|
|
10216
|
+
const collisions = detectAliasCollisions(noteName, existingAliases);
|
|
10217
|
+
for (const collision of collisions) {
|
|
10218
|
+
warnings.push({
|
|
10219
|
+
type: "alias_collision",
|
|
10220
|
+
message: `${collision.source === "name" ? "Note name" : "Alias"} "${collision.term}" collides with ${collision.collidedWith.matchType} of "${collision.collidedWith.name}" (${collision.collidedWith.path})`,
|
|
10221
|
+
suggestion: `This may cause ambiguous wikilink resolution`
|
|
10222
|
+
});
|
|
10223
|
+
}
|
|
10092
10224
|
let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(content, skipWikilinks, notePath);
|
|
10093
10225
|
let suggestInfo;
|
|
10094
10226
|
if (suggestOutgoingLinks && !skipWikilinks) {
|
|
@@ -10114,12 +10246,22 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
10114
10246
|
}
|
|
10115
10247
|
const hasAliases = frontmatter && "aliases" in frontmatter;
|
|
10116
10248
|
if (!hasAliases) {
|
|
10117
|
-
|
|
10118
|
-
|
|
10249
|
+
const aliasSuggestions = suggestAliases(noteName, existingAliases);
|
|
10250
|
+
if (aliasSuggestions.length > 0) {
|
|
10251
|
+
previewLines.push("");
|
|
10252
|
+
previewLines.push("Suggested aliases:");
|
|
10253
|
+
for (const s of aliasSuggestions) {
|
|
10254
|
+
previewLines.push(` - "${s.alias}" (${s.reason})`);
|
|
10255
|
+
}
|
|
10256
|
+
} else {
|
|
10257
|
+
previewLines.push("");
|
|
10258
|
+
previewLines.push('Tip: Add aliases to frontmatter for flexible wikilink matching (e.g., aliases: ["Short Name"])');
|
|
10259
|
+
}
|
|
10119
10260
|
}
|
|
10120
10261
|
return formatMcpResult(
|
|
10121
10262
|
successResult(notePath, `Created note: ${notePath}`, gitInfo, {
|
|
10122
|
-
preview: previewLines.join("\n")
|
|
10263
|
+
preview: previewLines.join("\n"),
|
|
10264
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
10123
10265
|
})
|
|
10124
10266
|
);
|
|
10125
10267
|
} catch (error) {
|
|
@@ -10139,9 +10281,6 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
10139
10281
|
},
|
|
10140
10282
|
async ({ path: notePath, confirm, commit }) => {
|
|
10141
10283
|
try {
|
|
10142
|
-
if (!confirm) {
|
|
10143
|
-
return formatMcpResult(errorResult(notePath, "Deletion requires explicit confirmation (confirm=true)"));
|
|
10144
|
-
}
|
|
10145
10284
|
if (!validatePath(vaultPath2, notePath)) {
|
|
10146
10285
|
return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
|
|
10147
10286
|
}
|
|
@@ -10149,10 +10288,38 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
10149
10288
|
if (existsError) {
|
|
10150
10289
|
return formatMcpResult(existsError);
|
|
10151
10290
|
}
|
|
10152
|
-
|
|
10291
|
+
let backlinkWarning;
|
|
10292
|
+
if (getIndex) {
|
|
10293
|
+
try {
|
|
10294
|
+
const index = getIndex();
|
|
10295
|
+
const backlinks = getBacklinksForNote(index, notePath);
|
|
10296
|
+
if (backlinks.length > 0) {
|
|
10297
|
+
const sources = backlinks.slice(0, 10).map((bl) => ` - ${bl.source}${bl.context ? ` ("${bl.context.slice(0, 60)}")` : ""}`).join("\n");
|
|
10298
|
+
backlinkWarning = `This note is referenced from ${backlinks.length} other note(s):
|
|
10299
|
+
${sources}`;
|
|
10300
|
+
if (backlinks.length > 10) {
|
|
10301
|
+
backlinkWarning += `
|
|
10302
|
+
... and ${backlinks.length - 10} more`;
|
|
10303
|
+
}
|
|
10304
|
+
}
|
|
10305
|
+
} catch {
|
|
10306
|
+
}
|
|
10307
|
+
}
|
|
10308
|
+
if (!confirm) {
|
|
10309
|
+
const previewLines = ["Deletion requires explicit confirmation (confirm=true)"];
|
|
10310
|
+
if (backlinkWarning) {
|
|
10311
|
+
previewLines.push("");
|
|
10312
|
+
previewLines.push("Warning: " + backlinkWarning);
|
|
10313
|
+
}
|
|
10314
|
+
return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
|
|
10315
|
+
}
|
|
10316
|
+
const fullPath = path17.join(vaultPath2, notePath);
|
|
10153
10317
|
await fs16.unlink(fullPath);
|
|
10154
10318
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Crank:Delete]");
|
|
10155
|
-
|
|
10319
|
+
const message = backlinkWarning ? `Deleted note: ${notePath}
|
|
10320
|
+
|
|
10321
|
+
Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
|
|
10322
|
+
return formatMcpResult(successResult(notePath, message, gitInfo));
|
|
10156
10323
|
} catch (error) {
|
|
10157
10324
|
return formatMcpResult(
|
|
10158
10325
|
errorResult(notePath, `Failed to delete note: ${error instanceof Error ? error.message : String(error)}`)
|
|
@@ -10166,7 +10333,7 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
10166
10333
|
init_writer();
|
|
10167
10334
|
import { z as z16 } from "zod";
|
|
10168
10335
|
import fs17 from "fs/promises";
|
|
10169
|
-
import
|
|
10336
|
+
import path18 from "path";
|
|
10170
10337
|
import matter6 from "gray-matter";
|
|
10171
10338
|
function escapeRegex(str) {
|
|
10172
10339
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -10185,7 +10352,7 @@ function extractWikilinks2(content) {
|
|
|
10185
10352
|
return wikilinks;
|
|
10186
10353
|
}
|
|
10187
10354
|
function getTitleFromPath(filePath) {
|
|
10188
|
-
return
|
|
10355
|
+
return path18.basename(filePath, ".md");
|
|
10189
10356
|
}
|
|
10190
10357
|
async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
10191
10358
|
const results = [];
|
|
@@ -10194,7 +10361,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
10194
10361
|
const files = [];
|
|
10195
10362
|
const entries = await fs17.readdir(dir, { withFileTypes: true });
|
|
10196
10363
|
for (const entry of entries) {
|
|
10197
|
-
const fullPath =
|
|
10364
|
+
const fullPath = path18.join(dir, entry.name);
|
|
10198
10365
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
10199
10366
|
files.push(...await scanDir(fullPath));
|
|
10200
10367
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -10205,7 +10372,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
10205
10372
|
}
|
|
10206
10373
|
const allFiles = await scanDir(vaultPath2);
|
|
10207
10374
|
for (const filePath of allFiles) {
|
|
10208
|
-
const relativePath =
|
|
10375
|
+
const relativePath = path18.relative(vaultPath2, filePath);
|
|
10209
10376
|
const content = await fs17.readFile(filePath, "utf-8");
|
|
10210
10377
|
const wikilinks = extractWikilinks2(content);
|
|
10211
10378
|
const matchingLinks = [];
|
|
@@ -10225,7 +10392,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
10225
10392
|
return results;
|
|
10226
10393
|
}
|
|
10227
10394
|
async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
|
|
10228
|
-
const fullPath =
|
|
10395
|
+
const fullPath = path18.join(vaultPath2, filePath);
|
|
10229
10396
|
const raw = await fs17.readFile(fullPath, "utf-8");
|
|
10230
10397
|
const parsed = matter6(raw);
|
|
10231
10398
|
let content = parsed.content;
|
|
@@ -10292,8 +10459,8 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10292
10459
|
};
|
|
10293
10460
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10294
10461
|
}
|
|
10295
|
-
const oldFullPath =
|
|
10296
|
-
const newFullPath =
|
|
10462
|
+
const oldFullPath = path18.join(vaultPath2, oldPath);
|
|
10463
|
+
const newFullPath = path18.join(vaultPath2, newPath);
|
|
10297
10464
|
try {
|
|
10298
10465
|
await fs17.access(oldFullPath);
|
|
10299
10466
|
} catch {
|
|
@@ -10343,7 +10510,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10343
10510
|
}
|
|
10344
10511
|
}
|
|
10345
10512
|
}
|
|
10346
|
-
const destDir =
|
|
10513
|
+
const destDir = path18.dirname(newFullPath);
|
|
10347
10514
|
await fs17.mkdir(destDir, { recursive: true });
|
|
10348
10515
|
await fs17.rename(oldFullPath, newFullPath);
|
|
10349
10516
|
let gitCommit;
|
|
@@ -10429,10 +10596,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10429
10596
|
if (sanitizedTitle !== newTitle) {
|
|
10430
10597
|
console.error(`[Crank] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
|
|
10431
10598
|
}
|
|
10432
|
-
const fullPath =
|
|
10433
|
-
const dir =
|
|
10434
|
-
const newPath = dir === "." ? `${sanitizedTitle}.md` :
|
|
10435
|
-
const newFullPath =
|
|
10599
|
+
const fullPath = path18.join(vaultPath2, notePath);
|
|
10600
|
+
const dir = path18.dirname(notePath);
|
|
10601
|
+
const newPath = dir === "." ? `${sanitizedTitle}.md` : path18.join(dir, `${sanitizedTitle}.md`);
|
|
10602
|
+
const newFullPath = path18.join(vaultPath2, newPath);
|
|
10436
10603
|
try {
|
|
10437
10604
|
await fs17.access(fullPath);
|
|
10438
10605
|
} catch {
|
|
@@ -10544,7 +10711,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
10544
10711
|
init_writer();
|
|
10545
10712
|
import { z as z17 } from "zod";
|
|
10546
10713
|
import fs18 from "fs/promises";
|
|
10547
|
-
import
|
|
10714
|
+
import path19 from "path";
|
|
10548
10715
|
function registerSystemTools2(server2, vaultPath2) {
|
|
10549
10716
|
server2.tool(
|
|
10550
10717
|
"vault_list_sections",
|
|
@@ -10564,7 +10731,7 @@ function registerSystemTools2(server2, vaultPath2) {
|
|
|
10564
10731
|
};
|
|
10565
10732
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10566
10733
|
}
|
|
10567
|
-
const fullPath =
|
|
10734
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
10568
10735
|
try {
|
|
10569
10736
|
await fs18.access(fullPath);
|
|
10570
10737
|
} catch {
|
|
@@ -10718,7 +10885,7 @@ init_schema();
|
|
|
10718
10885
|
// src/core/write/policy/parser.ts
|
|
10719
10886
|
init_schema();
|
|
10720
10887
|
import fs19 from "fs/promises";
|
|
10721
|
-
import
|
|
10888
|
+
import path20 from "path";
|
|
10722
10889
|
import matter7 from "gray-matter";
|
|
10723
10890
|
function parseYaml(content) {
|
|
10724
10891
|
const parsed = matter7(`---
|
|
@@ -10767,13 +10934,13 @@ async function loadPolicyFile(filePath) {
|
|
|
10767
10934
|
}
|
|
10768
10935
|
}
|
|
10769
10936
|
async function loadPolicy(vaultPath2, policyName) {
|
|
10770
|
-
const policiesDir =
|
|
10771
|
-
const policyPath =
|
|
10937
|
+
const policiesDir = path20.join(vaultPath2, ".claude", "policies");
|
|
10938
|
+
const policyPath = path20.join(policiesDir, `${policyName}.yaml`);
|
|
10772
10939
|
try {
|
|
10773
10940
|
await fs19.access(policyPath);
|
|
10774
10941
|
return loadPolicyFile(policyPath);
|
|
10775
10942
|
} catch {
|
|
10776
|
-
const ymlPath =
|
|
10943
|
+
const ymlPath = path20.join(policiesDir, `${policyName}.yml`);
|
|
10777
10944
|
try {
|
|
10778
10945
|
await fs19.access(ymlPath);
|
|
10779
10946
|
return loadPolicyFile(ymlPath);
|
|
@@ -10914,7 +11081,7 @@ init_conditions();
|
|
|
10914
11081
|
init_schema();
|
|
10915
11082
|
init_writer();
|
|
10916
11083
|
import fs21 from "fs/promises";
|
|
10917
|
-
import
|
|
11084
|
+
import path22 from "path";
|
|
10918
11085
|
init_constants();
|
|
10919
11086
|
async function executeStep(step, vaultPath2, context, conditionResults) {
|
|
10920
11087
|
const { execute, reason } = shouldStepExecute(step.when, conditionResults);
|
|
@@ -10983,7 +11150,7 @@ async function executeAddToSection(params, vaultPath2, context) {
|
|
|
10983
11150
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
10984
11151
|
const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
|
|
10985
11152
|
const maxSuggestions = Number(params.maxSuggestions) || 3;
|
|
10986
|
-
const fullPath =
|
|
11153
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
10987
11154
|
try {
|
|
10988
11155
|
await fs21.access(fullPath);
|
|
10989
11156
|
} catch {
|
|
@@ -11023,7 +11190,7 @@ async function executeRemoveFromSection(params, vaultPath2) {
|
|
|
11023
11190
|
const pattern = String(params.pattern || "");
|
|
11024
11191
|
const mode = params.mode || "first";
|
|
11025
11192
|
const useRegex = Boolean(params.useRegex);
|
|
11026
|
-
const fullPath =
|
|
11193
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
11027
11194
|
try {
|
|
11028
11195
|
await fs21.access(fullPath);
|
|
11029
11196
|
} catch {
|
|
@@ -11054,7 +11221,7 @@ async function executeReplaceInSection(params, vaultPath2, context) {
|
|
|
11054
11221
|
const mode = params.mode || "first";
|
|
11055
11222
|
const useRegex = Boolean(params.useRegex);
|
|
11056
11223
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
11057
|
-
const fullPath =
|
|
11224
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
11058
11225
|
try {
|
|
11059
11226
|
await fs21.access(fullPath);
|
|
11060
11227
|
} catch {
|
|
@@ -11097,7 +11264,7 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
11097
11264
|
if (!validatePath(vaultPath2, notePath)) {
|
|
11098
11265
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
11099
11266
|
}
|
|
11100
|
-
const fullPath =
|
|
11267
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
11101
11268
|
try {
|
|
11102
11269
|
await fs21.access(fullPath);
|
|
11103
11270
|
if (!overwrite) {
|
|
@@ -11105,7 +11272,7 @@ async function executeCreateNote(params, vaultPath2, context) {
|
|
|
11105
11272
|
}
|
|
11106
11273
|
} catch {
|
|
11107
11274
|
}
|
|
11108
|
-
const dir =
|
|
11275
|
+
const dir = path22.dirname(fullPath);
|
|
11109
11276
|
await fs21.mkdir(dir, { recursive: true });
|
|
11110
11277
|
const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
|
|
11111
11278
|
await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
|
|
@@ -11125,7 +11292,7 @@ async function executeDeleteNote(params, vaultPath2) {
|
|
|
11125
11292
|
if (!validatePath(vaultPath2, notePath)) {
|
|
11126
11293
|
return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
|
|
11127
11294
|
}
|
|
11128
|
-
const fullPath =
|
|
11295
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
11129
11296
|
try {
|
|
11130
11297
|
await fs21.access(fullPath);
|
|
11131
11298
|
} catch {
|
|
@@ -11142,7 +11309,7 @@ async function executeToggleTask(params, vaultPath2) {
|
|
|
11142
11309
|
const notePath = String(params.path || "");
|
|
11143
11310
|
const task = String(params.task || "");
|
|
11144
11311
|
const section = params.section ? String(params.section) : void 0;
|
|
11145
|
-
const fullPath =
|
|
11312
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
11146
11313
|
try {
|
|
11147
11314
|
await fs21.access(fullPath);
|
|
11148
11315
|
} catch {
|
|
@@ -11185,7 +11352,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
11185
11352
|
const completed = Boolean(params.completed);
|
|
11186
11353
|
const skipWikilinks = Boolean(params.skipWikilinks);
|
|
11187
11354
|
const preserveListNesting = params.preserveListNesting !== false;
|
|
11188
|
-
const fullPath =
|
|
11355
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
11189
11356
|
try {
|
|
11190
11357
|
await fs21.access(fullPath);
|
|
11191
11358
|
} catch {
|
|
@@ -11222,7 +11389,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
11222
11389
|
async function executeUpdateFrontmatter(params, vaultPath2) {
|
|
11223
11390
|
const notePath = String(params.path || "");
|
|
11224
11391
|
const updates = params.frontmatter || {};
|
|
11225
|
-
const fullPath =
|
|
11392
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
11226
11393
|
try {
|
|
11227
11394
|
await fs21.access(fullPath);
|
|
11228
11395
|
} catch {
|
|
@@ -11244,7 +11411,7 @@ async function executeAddFrontmatterField(params, vaultPath2) {
|
|
|
11244
11411
|
const notePath = String(params.path || "");
|
|
11245
11412
|
const key = String(params.key || "");
|
|
11246
11413
|
const value = params.value;
|
|
11247
|
-
const fullPath =
|
|
11414
|
+
const fullPath = path22.join(vaultPath2, notePath);
|
|
11248
11415
|
try {
|
|
11249
11416
|
await fs21.access(fullPath);
|
|
11250
11417
|
} catch {
|
|
@@ -11404,7 +11571,7 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
|
|
|
11404
11571
|
async function rollbackChanges(vaultPath2, originalContents, filesModified) {
|
|
11405
11572
|
for (const filePath of filesModified) {
|
|
11406
11573
|
const original = originalContents.get(filePath);
|
|
11407
|
-
const fullPath =
|
|
11574
|
+
const fullPath = path22.join(vaultPath2, filePath);
|
|
11408
11575
|
if (original === null) {
|
|
11409
11576
|
try {
|
|
11410
11577
|
await fs21.unlink(fullPath);
|
|
@@ -11459,9 +11626,9 @@ async function previewPolicy(policy, vaultPath2, variables) {
|
|
|
11459
11626
|
|
|
11460
11627
|
// src/core/write/policy/storage.ts
|
|
11461
11628
|
import fs22 from "fs/promises";
|
|
11462
|
-
import
|
|
11629
|
+
import path23 from "path";
|
|
11463
11630
|
function getPoliciesDir(vaultPath2) {
|
|
11464
|
-
return
|
|
11631
|
+
return path23.join(vaultPath2, ".claude", "policies");
|
|
11465
11632
|
}
|
|
11466
11633
|
async function ensurePoliciesDir(vaultPath2) {
|
|
11467
11634
|
const dir = getPoliciesDir(vaultPath2);
|
|
@@ -11476,7 +11643,7 @@ async function listPolicies(vaultPath2) {
|
|
|
11476
11643
|
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
|
|
11477
11644
|
continue;
|
|
11478
11645
|
}
|
|
11479
|
-
const filePath =
|
|
11646
|
+
const filePath = path23.join(dir, file);
|
|
11480
11647
|
const stat3 = await fs22.stat(filePath);
|
|
11481
11648
|
const content = await fs22.readFile(filePath, "utf-8");
|
|
11482
11649
|
const metadata = extractPolicyMetadata(content);
|
|
@@ -11499,8 +11666,8 @@ async function listPolicies(vaultPath2) {
|
|
|
11499
11666
|
}
|
|
11500
11667
|
async function getPolicyPath(vaultPath2, policyName) {
|
|
11501
11668
|
const dir = getPoliciesDir(vaultPath2);
|
|
11502
|
-
const yamlPath =
|
|
11503
|
-
const ymlPath =
|
|
11669
|
+
const yamlPath = path23.join(dir, `${policyName}.yaml`);
|
|
11670
|
+
const ymlPath = path23.join(dir, `${policyName}.yml`);
|
|
11504
11671
|
try {
|
|
11505
11672
|
await fs22.access(yamlPath);
|
|
11506
11673
|
return yamlPath;
|
|
@@ -11517,7 +11684,7 @@ async function savePolicy(vaultPath2, policy, overwrite = false) {
|
|
|
11517
11684
|
const dir = getPoliciesDir(vaultPath2);
|
|
11518
11685
|
await ensurePoliciesDir(vaultPath2);
|
|
11519
11686
|
const filename = `${policy.name}.yaml`;
|
|
11520
|
-
const filePath =
|
|
11687
|
+
const filePath = path23.join(dir, filename);
|
|
11521
11688
|
if (!overwrite) {
|
|
11522
11689
|
try {
|
|
11523
11690
|
await fs22.access(filePath);
|
|
@@ -11556,7 +11723,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
|
|
|
11556
11723
|
const dir = getPoliciesDir(vaultPath2);
|
|
11557
11724
|
await ensurePoliciesDir(vaultPath2);
|
|
11558
11725
|
const filename = `${policyName}.yaml`;
|
|
11559
|
-
const filePath =
|
|
11726
|
+
const filePath = path23.join(dir, filename);
|
|
11560
11727
|
if (!overwrite) {
|
|
11561
11728
|
try {
|
|
11562
11729
|
await fs22.access(filePath);
|
|
@@ -12554,7 +12721,7 @@ registerMigrationTools(server, () => vaultIndex, () => vaultPath);
|
|
|
12554
12721
|
registerMutationTools(server, vaultPath);
|
|
12555
12722
|
registerTaskTools(server, vaultPath);
|
|
12556
12723
|
registerFrontmatterTools(server, vaultPath);
|
|
12557
|
-
registerNoteTools(server, vaultPath);
|
|
12724
|
+
registerNoteTools(server, vaultPath, () => vaultIndex);
|
|
12558
12725
|
registerMoveNoteTools(server, vaultPath);
|
|
12559
12726
|
registerSystemTools2(server, vaultPath);
|
|
12560
12727
|
registerPolicyTools(server, vaultPath);
|
|
@@ -12566,6 +12733,7 @@ async function main() {
|
|
|
12566
12733
|
try {
|
|
12567
12734
|
stateDb = openStateDb(vaultPath);
|
|
12568
12735
|
console.error("[Memory] StateDb initialized");
|
|
12736
|
+
setFTS5Database(stateDb.db);
|
|
12569
12737
|
setCrankStateDb(stateDb);
|
|
12570
12738
|
await initializeEntityIndex(vaultPath);
|
|
12571
12739
|
} catch (err) {
|
|
@@ -12723,8 +12891,8 @@ async function runPostIndexWork(index) {
|
|
|
12723
12891
|
}
|
|
12724
12892
|
});
|
|
12725
12893
|
let rebuildTimer;
|
|
12726
|
-
legacyWatcher.on("all", (event,
|
|
12727
|
-
if (!
|
|
12894
|
+
legacyWatcher.on("all", (event, path24) => {
|
|
12895
|
+
if (!path24.endsWith(".md")) return;
|
|
12728
12896
|
clearTimeout(rebuildTimer);
|
|
12729
12897
|
rebuildTimer = setTimeout(() => {
|
|
12730
12898
|
console.error("[Memory] Rebuilding index (file changed)");
|