@velvetmonkey/flywheel-memory 2.0.64 → 2.0.66
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 +103 -48
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ var init_constants = __esm({
|
|
|
61
61
|
import fs18 from "fs/promises";
|
|
62
62
|
import path20 from "path";
|
|
63
63
|
import matter5 from "gray-matter";
|
|
64
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
64
65
|
function isSensitivePath(filePath) {
|
|
65
66
|
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
66
67
|
return SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath));
|
|
@@ -471,6 +472,9 @@ async function validatePathSecure(vaultPath2, notePath) {
|
|
|
471
472
|
}
|
|
472
473
|
return { valid: true };
|
|
473
474
|
}
|
|
475
|
+
function computeContentHash(rawContent) {
|
|
476
|
+
return createHash2("sha256").update(rawContent).digest("hex").slice(0, 16);
|
|
477
|
+
}
|
|
474
478
|
async function readVaultFile(vaultPath2, notePath) {
|
|
475
479
|
if (!validatePath(vaultPath2, notePath)) {
|
|
476
480
|
throw new Error("Invalid path: path traversal not allowed");
|
|
@@ -480,6 +484,7 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
480
484
|
fs18.readFile(fullPath, "utf-8"),
|
|
481
485
|
fs18.stat(fullPath)
|
|
482
486
|
]);
|
|
487
|
+
const contentHash2 = computeContentHash(rawContent);
|
|
483
488
|
const lineEnding = detectLineEnding(rawContent);
|
|
484
489
|
const normalizedContent = normalizeLineEndings(rawContent);
|
|
485
490
|
const parsed = matter5(normalizedContent);
|
|
@@ -489,7 +494,8 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
489
494
|
frontmatter,
|
|
490
495
|
rawContent,
|
|
491
496
|
lineEnding,
|
|
492
|
-
mtimeMs: stat4.mtimeMs
|
|
497
|
+
mtimeMs: stat4.mtimeMs,
|
|
498
|
+
contentHash: contentHash2
|
|
493
499
|
};
|
|
494
500
|
}
|
|
495
501
|
function deepCloneFrontmatter(obj) {
|
|
@@ -523,12 +529,19 @@ function deepCloneFrontmatter(obj) {
|
|
|
523
529
|
}
|
|
524
530
|
return cloned;
|
|
525
531
|
}
|
|
526
|
-
async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEnding = "LF") {
|
|
532
|
+
async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEnding = "LF", expectedHash) {
|
|
527
533
|
const validation = await validatePathSecure(vaultPath2, notePath);
|
|
528
534
|
if (!validation.valid) {
|
|
529
535
|
throw new Error(`Invalid path: ${validation.reason}`);
|
|
530
536
|
}
|
|
531
537
|
const fullPath = path20.join(vaultPath2, notePath);
|
|
538
|
+
if (expectedHash) {
|
|
539
|
+
const currentRaw = await fs18.readFile(fullPath, "utf-8");
|
|
540
|
+
const currentHash = computeContentHash(currentRaw);
|
|
541
|
+
if (currentHash !== expectedHash) {
|
|
542
|
+
throw new WriteConflictError(notePath);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
532
545
|
let output = matter5.stringify(content, frontmatter);
|
|
533
546
|
output = normalizeTrailingNewline(output);
|
|
534
547
|
output = convertLineEndings(output, lineEnding);
|
|
@@ -693,7 +706,7 @@ function injectMutationMetadata(frontmatter, scoping) {
|
|
|
693
706
|
}
|
|
694
707
|
return frontmatter;
|
|
695
708
|
}
|
|
696
|
-
var SENSITIVE_PATH_PATTERNS, REDOS_PATTERNS, MAX_REGEX_LENGTH, EMPTY_PLACEHOLDER_PATTERNS, DiagnosticError;
|
|
709
|
+
var SENSITIVE_PATH_PATTERNS, REDOS_PATTERNS, MAX_REGEX_LENGTH, EMPTY_PLACEHOLDER_PATTERNS, WriteConflictError, DiagnosticError;
|
|
697
710
|
var init_writer = __esm({
|
|
698
711
|
"src/core/write/writer.ts"() {
|
|
699
712
|
"use strict";
|
|
@@ -799,6 +812,13 @@ var init_writer = __esm({
|
|
|
799
812
|
/^\*\s*$/
|
|
800
813
|
// "* " (asterisk bullet placeholder)
|
|
801
814
|
];
|
|
815
|
+
WriteConflictError = class extends Error {
|
|
816
|
+
constructor(notePath) {
|
|
817
|
+
super(`Write conflict on ${notePath}: file was modified externally since it was read. Re-read and retry.`);
|
|
818
|
+
this.notePath = notePath;
|
|
819
|
+
this.name = "WriteConflictError";
|
|
820
|
+
}
|
|
821
|
+
};
|
|
802
822
|
DiagnosticError = class extends Error {
|
|
803
823
|
diagnostic;
|
|
804
824
|
constructor(message, diagnostic) {
|
|
@@ -6383,6 +6403,8 @@ var TYPE_BOOST = {
|
|
|
6383
6403
|
// Crafts, sports
|
|
6384
6404
|
finance: 1,
|
|
6385
6405
|
// Accounts, budgets
|
|
6406
|
+
periodical: 1,
|
|
6407
|
+
// Daily/weekly/monthly notes — low boost
|
|
6386
6408
|
technologies: 0,
|
|
6387
6409
|
// Common, avoid over-suggesting
|
|
6388
6410
|
acronyms: 0,
|
|
@@ -7360,8 +7382,10 @@ function serverLog(component, message, level = "info") {
|
|
|
7360
7382
|
if (buffer.length > MAX_ENTRIES) {
|
|
7361
7383
|
buffer.shift();
|
|
7362
7384
|
}
|
|
7385
|
+
const now = /* @__PURE__ */ new Date();
|
|
7386
|
+
const hms = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
|
|
7363
7387
|
const prefix = level === "error" ? "[Memory] ERROR" : level === "warn" ? "[Memory] WARN" : "[Memory]";
|
|
7364
|
-
console.error(`${prefix} [${component}] ${message}`);
|
|
7388
|
+
console.error(`${prefix} [${hms}] [${component}] ${message}`);
|
|
7365
7389
|
}
|
|
7366
7390
|
function getServerLog(options = {}) {
|
|
7367
7391
|
const { since, component, limit = 100 } = options;
|
|
@@ -13228,7 +13252,7 @@ async function withVaultFile(options, operation) {
|
|
|
13228
13252
|
return formatMcpResult(existsError);
|
|
13229
13253
|
}
|
|
13230
13254
|
const runMutation = async () => {
|
|
13231
|
-
const { content, frontmatter: frontmatter2, lineEnding: lineEnding2,
|
|
13255
|
+
const { content, frontmatter: frontmatter2, lineEnding: lineEnding2, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
13232
13256
|
const writeStateDb = getWriteStateDb();
|
|
13233
13257
|
if (writeStateDb) {
|
|
13234
13258
|
processImplicitFeedback(writeStateDb, notePath, content);
|
|
@@ -13250,7 +13274,7 @@ async function withVaultFile(options, operation) {
|
|
|
13250
13274
|
notePath
|
|
13251
13275
|
};
|
|
13252
13276
|
const opResult2 = await operation(ctx);
|
|
13253
|
-
return { opResult: opResult2, frontmatter: frontmatter2, lineEnding: lineEnding2,
|
|
13277
|
+
return { opResult: opResult2, frontmatter: frontmatter2, lineEnding: lineEnding2, contentHash: contentHash2 };
|
|
13254
13278
|
};
|
|
13255
13279
|
let result = await runMutation();
|
|
13256
13280
|
if ("error" in result) {
|
|
@@ -13267,20 +13291,11 @@ async function withVaultFile(options, operation) {
|
|
|
13267
13291
|
});
|
|
13268
13292
|
return formatMcpResult(dryResult);
|
|
13269
13293
|
}
|
|
13270
|
-
const fullPath = path21.join(vaultPath2, notePath);
|
|
13271
|
-
const statBefore = await fs19.stat(fullPath);
|
|
13272
|
-
if (statBefore.mtimeMs !== result.mtimeMs) {
|
|
13273
|
-
console.warn(`[withVaultFile] External modification detected on ${notePath}, re-reading and retrying`);
|
|
13274
|
-
result = await runMutation();
|
|
13275
|
-
if ("error" in result) {
|
|
13276
|
-
return formatMcpResult(result.error);
|
|
13277
|
-
}
|
|
13278
|
-
}
|
|
13279
13294
|
let finalFrontmatter = opResult.updatedFrontmatter ?? frontmatter;
|
|
13280
13295
|
if (scoping && (scoping.agent_id || scoping.session_id)) {
|
|
13281
13296
|
finalFrontmatter = injectMutationMetadata(finalFrontmatter, scoping);
|
|
13282
13297
|
}
|
|
13283
|
-
await writeVaultFile(vaultPath2, notePath, opResult.updatedContent, finalFrontmatter, lineEnding);
|
|
13298
|
+
await writeVaultFile(vaultPath2, notePath, opResult.updatedContent, finalFrontmatter, lineEnding, result.contentHash);
|
|
13284
13299
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, commitPrefix);
|
|
13285
13300
|
const successRes = successResult(notePath, opResult.message, gitInfo, {
|
|
13286
13301
|
preview: opResult.preview,
|
|
@@ -13291,6 +13306,13 @@ async function withVaultFile(options, operation) {
|
|
|
13291
13306
|
return formatMcpResult(successRes);
|
|
13292
13307
|
} catch (error) {
|
|
13293
13308
|
const extras = {};
|
|
13309
|
+
if (error instanceof WriteConflictError) {
|
|
13310
|
+
extras.warnings = [{
|
|
13311
|
+
type: "write_conflict",
|
|
13312
|
+
message: error.message,
|
|
13313
|
+
suggestion: "The file was modified while processing. Re-read and retry."
|
|
13314
|
+
}];
|
|
13315
|
+
}
|
|
13294
13316
|
if (error instanceof DiagnosticError) {
|
|
13295
13317
|
extras.diagnostic = error.diagnostic;
|
|
13296
13318
|
}
|
|
@@ -13309,7 +13331,7 @@ async function withVaultFrontmatter(options, operation) {
|
|
|
13309
13331
|
if (existsError) {
|
|
13310
13332
|
return formatMcpResult(existsError);
|
|
13311
13333
|
}
|
|
13312
|
-
const { content, frontmatter, lineEnding } = await readVaultFile(vaultPath2, notePath);
|
|
13334
|
+
const { content, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
13313
13335
|
const ctx = { content, frontmatter, lineEnding, vaultPath: vaultPath2, notePath };
|
|
13314
13336
|
const opResult = await operation(ctx);
|
|
13315
13337
|
if (dryRun) {
|
|
@@ -13319,16 +13341,25 @@ async function withVaultFrontmatter(options, operation) {
|
|
|
13319
13341
|
});
|
|
13320
13342
|
return formatMcpResult(result2);
|
|
13321
13343
|
}
|
|
13322
|
-
await writeVaultFile(vaultPath2, notePath, content, opResult.updatedFrontmatter, lineEnding);
|
|
13344
|
+
await writeVaultFile(vaultPath2, notePath, content, opResult.updatedFrontmatter, lineEnding, contentHash2);
|
|
13323
13345
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, commitPrefix);
|
|
13324
13346
|
const result = successResult(notePath, opResult.message, gitInfo, {
|
|
13325
13347
|
preview: opResult.preview
|
|
13326
13348
|
});
|
|
13327
13349
|
return formatMcpResult(result);
|
|
13328
13350
|
} catch (error) {
|
|
13351
|
+
const extras = {};
|
|
13352
|
+
if (error instanceof WriteConflictError) {
|
|
13353
|
+
extras.warnings = [{
|
|
13354
|
+
type: "write_conflict",
|
|
13355
|
+
message: error.message,
|
|
13356
|
+
suggestion: "The file was modified while processing. Re-read and retry."
|
|
13357
|
+
}];
|
|
13358
|
+
}
|
|
13329
13359
|
const result = errorResult(
|
|
13330
13360
|
notePath,
|
|
13331
|
-
`Failed to ${actionDescription}: ${error instanceof Error ? error.message : String(error)}
|
|
13361
|
+
`Failed to ${actionDescription}: ${error instanceof Error ? error.message : String(error)}`,
|
|
13362
|
+
extras
|
|
13332
13363
|
);
|
|
13333
13364
|
return formatMcpResult(result);
|
|
13334
13365
|
}
|
|
@@ -13703,7 +13734,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
13703
13734
|
if (existsError) {
|
|
13704
13735
|
return formatMcpResult(existsError);
|
|
13705
13736
|
}
|
|
13706
|
-
const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
|
|
13737
|
+
const { content: fileContent, frontmatter, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
13707
13738
|
let sectionBoundary;
|
|
13708
13739
|
if (section) {
|
|
13709
13740
|
const found = findSection(fileContent, section);
|
|
@@ -13740,7 +13771,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
13740
13771
|
if (agent_id || session_id) {
|
|
13741
13772
|
finalFrontmatter = injectMutationMetadata(frontmatter, { agent_id, session_id });
|
|
13742
13773
|
}
|
|
13743
|
-
await writeVaultFile(vaultPath2, notePath, toggleResult.content, finalFrontmatter);
|
|
13774
|
+
await writeVaultFile(vaultPath2, notePath, toggleResult.content, finalFrontmatter, "LF", contentHash2);
|
|
13744
13775
|
await updateTaskCacheForFile(vaultPath2, notePath).catch(() => {
|
|
13745
13776
|
});
|
|
13746
13777
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Task]");
|
|
@@ -13750,8 +13781,16 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
13750
13781
|
})
|
|
13751
13782
|
);
|
|
13752
13783
|
} catch (error) {
|
|
13784
|
+
const extras = {};
|
|
13785
|
+
if (error instanceof WriteConflictError) {
|
|
13786
|
+
extras.warnings = [{
|
|
13787
|
+
type: "write_conflict",
|
|
13788
|
+
message: error.message,
|
|
13789
|
+
suggestion: "The file was modified while processing. Re-read and retry."
|
|
13790
|
+
}];
|
|
13791
|
+
}
|
|
13753
13792
|
return formatMcpResult(
|
|
13754
|
-
errorResult(notePath, `Failed to toggle task: ${error instanceof Error ? error.message : String(error)}
|
|
13793
|
+
errorResult(notePath, `Failed to toggle task: ${error instanceof Error ? error.message : String(error)}`, extras)
|
|
13755
13794
|
);
|
|
13756
13795
|
}
|
|
13757
13796
|
}
|
|
@@ -14177,10 +14216,8 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
|
|
|
14177
14216
|
return results;
|
|
14178
14217
|
}
|
|
14179
14218
|
async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
|
|
14180
|
-
const
|
|
14181
|
-
|
|
14182
|
-
const parsed = matter6(raw);
|
|
14183
|
-
let content = parsed.content;
|
|
14219
|
+
const { content: fileContent, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, filePath);
|
|
14220
|
+
let content = fileContent;
|
|
14184
14221
|
let totalUpdated = 0;
|
|
14185
14222
|
for (const oldTitle of oldTitles) {
|
|
14186
14223
|
const pattern = new RegExp(
|
|
@@ -14193,7 +14230,7 @@ async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle)
|
|
|
14193
14230
|
});
|
|
14194
14231
|
}
|
|
14195
14232
|
if (totalUpdated > 0) {
|
|
14196
|
-
await writeVaultFile(vaultPath2, filePath, content,
|
|
14233
|
+
await writeVaultFile(vaultPath2, filePath, content, frontmatter, lineEnding, contentHash2);
|
|
14197
14234
|
return { updated: true, linksUpdated: totalUpdated };
|
|
14198
14235
|
}
|
|
14199
14236
|
return { updated: false, linksUpdated: 0 };
|
|
@@ -14579,10 +14616,12 @@ function registerMergeTools(server2, vaultPath2) {
|
|
|
14579
14616
|
}
|
|
14580
14617
|
let targetContent;
|
|
14581
14618
|
let targetFrontmatter;
|
|
14619
|
+
let targetContentHash;
|
|
14582
14620
|
try {
|
|
14583
14621
|
const target = await readVaultFile(vaultPath2, target_path);
|
|
14584
14622
|
targetContent = target.content;
|
|
14585
14623
|
targetFrontmatter = target.frontmatter;
|
|
14624
|
+
targetContentHash = target.contentHash;
|
|
14586
14625
|
} catch {
|
|
14587
14626
|
const result2 = {
|
|
14588
14627
|
success: false,
|
|
@@ -14657,7 +14696,7 @@ ${trimmedSource}`;
|
|
|
14657
14696
|
};
|
|
14658
14697
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
14659
14698
|
}
|
|
14660
|
-
await writeVaultFile(vaultPath2, target_path, targetContent, targetFrontmatter);
|
|
14699
|
+
await writeVaultFile(vaultPath2, target_path, targetContent, targetFrontmatter, "LF", targetContentHash);
|
|
14661
14700
|
const fullSourcePath = `${vaultPath2}/${source_path}`;
|
|
14662
14701
|
await fs23.unlink(fullSourcePath);
|
|
14663
14702
|
initializeEntityIndex(vaultPath2).catch((err) => {
|
|
@@ -14675,7 +14714,14 @@ ${trimmedSource}`;
|
|
|
14675
14714
|
const result = {
|
|
14676
14715
|
success: false,
|
|
14677
14716
|
message: `Failed to merge entities: ${error instanceof Error ? error.message : String(error)}`,
|
|
14678
|
-
path: source_path
|
|
14717
|
+
path: source_path,
|
|
14718
|
+
...error instanceof WriteConflictError ? {
|
|
14719
|
+
warnings: [{
|
|
14720
|
+
type: "write_conflict",
|
|
14721
|
+
message: error.message,
|
|
14722
|
+
suggestion: "The file was modified while processing. Re-read and retry."
|
|
14723
|
+
}]
|
|
14724
|
+
} : {}
|
|
14679
14725
|
};
|
|
14680
14726
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
14681
14727
|
}
|
|
@@ -14701,10 +14747,12 @@ ${trimmedSource}`;
|
|
|
14701
14747
|
}
|
|
14702
14748
|
let targetContent;
|
|
14703
14749
|
let targetFrontmatter;
|
|
14750
|
+
let absorbTargetHash;
|
|
14704
14751
|
try {
|
|
14705
14752
|
const target = await readVaultFile(vaultPath2, target_path);
|
|
14706
14753
|
targetContent = target.content;
|
|
14707
14754
|
targetFrontmatter = target.frontmatter;
|
|
14755
|
+
absorbTargetHash = target.contentHash;
|
|
14708
14756
|
} catch {
|
|
14709
14757
|
const result2 = {
|
|
14710
14758
|
success: false,
|
|
@@ -14730,7 +14778,7 @@ ${trimmedSource}`;
|
|
|
14730
14778
|
modifiedFiles.push(backlink.path);
|
|
14731
14779
|
}
|
|
14732
14780
|
} else {
|
|
14733
|
-
await writeVaultFile(vaultPath2, target_path, targetContent, targetFrontmatter);
|
|
14781
|
+
await writeVaultFile(vaultPath2, target_path, targetContent, targetFrontmatter, "LF", absorbTargetHash);
|
|
14734
14782
|
for (const backlink of backlinks) {
|
|
14735
14783
|
if (backlink.path === target_path) continue;
|
|
14736
14784
|
let fileData;
|
|
@@ -14756,7 +14804,7 @@ ${trimmedSource}`;
|
|
|
14756
14804
|
return `[[${targetTitle}|${source_name}]]`;
|
|
14757
14805
|
});
|
|
14758
14806
|
if (linksUpdated > 0) {
|
|
14759
|
-
await writeVaultFile(vaultPath2, backlink.path, content, fileData.frontmatter);
|
|
14807
|
+
await writeVaultFile(vaultPath2, backlink.path, content, fileData.frontmatter, fileData.lineEnding, fileData.contentHash);
|
|
14760
14808
|
totalBacklinksUpdated += linksUpdated;
|
|
14761
14809
|
modifiedFiles.push(backlink.path);
|
|
14762
14810
|
}
|
|
@@ -14787,7 +14835,14 @@ ${trimmedSource}`;
|
|
|
14787
14835
|
const result = {
|
|
14788
14836
|
success: false,
|
|
14789
14837
|
message: `Failed to absorb as alias: ${error instanceof Error ? error.message : String(error)}`,
|
|
14790
|
-
path: target_path
|
|
14838
|
+
path: target_path,
|
|
14839
|
+
...error instanceof WriteConflictError ? {
|
|
14840
|
+
warnings: [{
|
|
14841
|
+
type: "write_conflict",
|
|
14842
|
+
message: error.message,
|
|
14843
|
+
suggestion: "The file was modified while processing. Re-read and retry."
|
|
14844
|
+
}]
|
|
14845
|
+
} : {}
|
|
14791
14846
|
};
|
|
14792
14847
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
14793
14848
|
}
|
|
@@ -15189,7 +15244,7 @@ async function executeAddToSection(params, vaultPath2, context) {
|
|
|
15189
15244
|
} catch {
|
|
15190
15245
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
15191
15246
|
}
|
|
15192
|
-
const { content: fileContent, frontmatter, lineEnding } = await readVaultFile(vaultPath2, notePath);
|
|
15247
|
+
const { content: fileContent, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
15193
15248
|
const sectionBoundary = findSection(fileContent, section);
|
|
15194
15249
|
if (!sectionBoundary) {
|
|
15195
15250
|
return { success: false, message: `Section '${section}' not found`, path: notePath };
|
|
@@ -15209,7 +15264,7 @@ async function executeAddToSection(params, vaultPath2, context) {
|
|
|
15209
15264
|
position,
|
|
15210
15265
|
{ preserveListNesting }
|
|
15211
15266
|
);
|
|
15212
|
-
await writeVaultFile(vaultPath2, notePath, updatedContent, frontmatter, lineEnding);
|
|
15267
|
+
await writeVaultFile(vaultPath2, notePath, updatedContent, frontmatter, lineEnding, contentHash2);
|
|
15213
15268
|
return {
|
|
15214
15269
|
success: true,
|
|
15215
15270
|
message: `Added content to section "${sectionBoundary.name}" in ${notePath}`,
|
|
@@ -15229,7 +15284,7 @@ async function executeRemoveFromSection(params, vaultPath2) {
|
|
|
15229
15284
|
} catch {
|
|
15230
15285
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
15231
15286
|
}
|
|
15232
|
-
const { content: fileContent, frontmatter, lineEnding } = await readVaultFile(vaultPath2, notePath);
|
|
15287
|
+
const { content: fileContent, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
15233
15288
|
const sectionBoundary = findSection(fileContent, section);
|
|
15234
15289
|
if (!sectionBoundary) {
|
|
15235
15290
|
return { success: false, message: `Section '${section}' not found`, path: notePath };
|
|
@@ -15238,7 +15293,7 @@ async function executeRemoveFromSection(params, vaultPath2) {
|
|
|
15238
15293
|
if (removeResult.removedCount === 0) {
|
|
15239
15294
|
return { success: false, message: `No content matching "${pattern}" found`, path: notePath };
|
|
15240
15295
|
}
|
|
15241
|
-
await writeVaultFile(vaultPath2, notePath, removeResult.content, frontmatter, lineEnding);
|
|
15296
|
+
await writeVaultFile(vaultPath2, notePath, removeResult.content, frontmatter, lineEnding, contentHash2);
|
|
15242
15297
|
return {
|
|
15243
15298
|
success: true,
|
|
15244
15299
|
message: `Removed ${removeResult.removedCount} line(s) from section "${sectionBoundary.name}"`,
|
|
@@ -15260,7 +15315,7 @@ async function executeReplaceInSection(params, vaultPath2, context) {
|
|
|
15260
15315
|
} catch {
|
|
15261
15316
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
15262
15317
|
}
|
|
15263
|
-
const { content: fileContent, frontmatter, lineEnding } = await readVaultFile(vaultPath2, notePath);
|
|
15318
|
+
const { content: fileContent, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
15264
15319
|
const sectionBoundary = findSection(fileContent, section);
|
|
15265
15320
|
if (!sectionBoundary) {
|
|
15266
15321
|
return { success: false, message: `Section '${section}' not found`, path: notePath };
|
|
@@ -15277,7 +15332,7 @@ async function executeReplaceInSection(params, vaultPath2, context) {
|
|
|
15277
15332
|
if (replaceResult.replacedCount === 0) {
|
|
15278
15333
|
return { success: false, message: `No content matching "${search}" found`, path: notePath };
|
|
15279
15334
|
}
|
|
15280
|
-
await writeVaultFile(vaultPath2, notePath, replaceResult.content, frontmatter, lineEnding);
|
|
15335
|
+
await writeVaultFile(vaultPath2, notePath, replaceResult.content, frontmatter, lineEnding, contentHash2);
|
|
15281
15336
|
return {
|
|
15282
15337
|
success: true,
|
|
15283
15338
|
message: `Replaced ${replaceResult.replacedCount} occurrence(s) in section "${sectionBoundary.name}"`,
|
|
@@ -15348,7 +15403,7 @@ async function executeToggleTask(params, vaultPath2) {
|
|
|
15348
15403
|
} catch {
|
|
15349
15404
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
15350
15405
|
}
|
|
15351
|
-
const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
|
|
15406
|
+
const { content: fileContent, frontmatter, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
15352
15407
|
let sectionBoundary;
|
|
15353
15408
|
if (section) {
|
|
15354
15409
|
sectionBoundary = findSection(fileContent, section);
|
|
@@ -15367,7 +15422,7 @@ async function executeToggleTask(params, vaultPath2) {
|
|
|
15367
15422
|
if (!toggleResult) {
|
|
15368
15423
|
return { success: false, message: "Failed to toggle task", path: notePath };
|
|
15369
15424
|
}
|
|
15370
|
-
await writeVaultFile(vaultPath2, notePath, toggleResult.content, frontmatter);
|
|
15425
|
+
await writeVaultFile(vaultPath2, notePath, toggleResult.content, frontmatter, "LF", contentHash2);
|
|
15371
15426
|
const newStatus = toggleResult.newState ? "completed" : "incomplete";
|
|
15372
15427
|
const checkbox = toggleResult.newState ? "[x]" : "[ ]";
|
|
15373
15428
|
return {
|
|
@@ -15391,7 +15446,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
15391
15446
|
} catch {
|
|
15392
15447
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
15393
15448
|
}
|
|
15394
|
-
const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
|
|
15449
|
+
const { content: fileContent, frontmatter, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
15395
15450
|
const sectionBoundary = findSection(fileContent, section);
|
|
15396
15451
|
if (!sectionBoundary) {
|
|
15397
15452
|
return { success: false, message: `Section not found: ${section}`, path: notePath };
|
|
@@ -15411,7 +15466,7 @@ async function executeAddTask(params, vaultPath2, context) {
|
|
|
15411
15466
|
position,
|
|
15412
15467
|
{ preserveListNesting }
|
|
15413
15468
|
);
|
|
15414
|
-
await writeVaultFile(vaultPath2, notePath, updatedContent, frontmatter);
|
|
15469
|
+
await writeVaultFile(vaultPath2, notePath, updatedContent, frontmatter, "LF", contentHash2);
|
|
15415
15470
|
return {
|
|
15416
15471
|
success: true,
|
|
15417
15472
|
message: `Added task to section "${sectionBoundary.name}"`,
|
|
@@ -15428,9 +15483,9 @@ async function executeUpdateFrontmatter(params, vaultPath2) {
|
|
|
15428
15483
|
} catch {
|
|
15429
15484
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
15430
15485
|
}
|
|
15431
|
-
const { content, frontmatter } = await readVaultFile(vaultPath2, notePath);
|
|
15486
|
+
const { content, frontmatter, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
15432
15487
|
const updatedFrontmatter = { ...frontmatter, ...updates };
|
|
15433
|
-
await writeVaultFile(vaultPath2, notePath, content, updatedFrontmatter);
|
|
15488
|
+
await writeVaultFile(vaultPath2, notePath, content, updatedFrontmatter, "LF", contentHash2);
|
|
15434
15489
|
const updatedKeys = Object.keys(updates);
|
|
15435
15490
|
const preview = updatedKeys.map((k) => `${k}: ${JSON.stringify(updates[k])}`).join("\n");
|
|
15436
15491
|
return {
|
|
@@ -15450,12 +15505,12 @@ async function executeAddFrontmatterField(params, vaultPath2) {
|
|
|
15450
15505
|
} catch {
|
|
15451
15506
|
return { success: false, message: `File not found: ${notePath}`, path: notePath };
|
|
15452
15507
|
}
|
|
15453
|
-
const { content, frontmatter } = await readVaultFile(vaultPath2, notePath);
|
|
15508
|
+
const { content, frontmatter, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
15454
15509
|
if (key in frontmatter) {
|
|
15455
15510
|
return { success: false, message: `Field "${key}" already exists`, path: notePath };
|
|
15456
15511
|
}
|
|
15457
15512
|
const updatedFrontmatter = { ...frontmatter, [key]: value };
|
|
15458
|
-
await writeVaultFile(vaultPath2, notePath, content, updatedFrontmatter);
|
|
15513
|
+
await writeVaultFile(vaultPath2, notePath, content, updatedFrontmatter, "LF", contentHash2);
|
|
15459
15514
|
return {
|
|
15460
15515
|
success: true,
|
|
15461
15516
|
message: `Added frontmatter field "${key}"`,
|
|
@@ -18922,7 +18977,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
18922
18977
|
|
|
18923
18978
|
// src/index.ts
|
|
18924
18979
|
import * as fs31 from "node:fs/promises";
|
|
18925
|
-
import { createHash as
|
|
18980
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
18926
18981
|
|
|
18927
18982
|
// src/resources/vault.ts
|
|
18928
18983
|
function registerVaultResources(server2, getIndex) {
|
|
@@ -19698,7 +19753,7 @@ async function runPostIndexWork(index) {
|
|
|
19698
19753
|
}
|
|
19699
19754
|
try {
|
|
19700
19755
|
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
19701
|
-
const hash =
|
|
19756
|
+
const hash = createHash3("sha256").update(content).digest("hex").slice(0, 16);
|
|
19702
19757
|
if (lastContentHashes.get(event.path) === hash) {
|
|
19703
19758
|
serverLog("watcher", `Hash unchanged, skipping: ${event.path}`);
|
|
19704
19759
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.66",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
55
|
-
"@velvetmonkey/vault-core": "2.0.
|
|
55
|
+
"@velvetmonkey/vault-core": "2.0.66",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|