@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.
Files changed (2) hide show
  1. package/dist/index.js +103 -48
  2. 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, mtimeMs } = await readVaultFile(vaultPath2, notePath);
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, mtimeMs };
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 fullPath = path24.join(vaultPath2, filePath);
14181
- const raw = await fs22.readFile(fullPath, "utf-8");
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, parsed.data);
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 createHash2 } from "node:crypto";
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 = createHash2("sha256").update(content).digest("hex").slice(0, 16);
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.64",
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.64",
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",