seekstone 0.13.1 → 0.14.0

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/README.md CHANGED
@@ -135,7 +135,7 @@ Other MCP clients (Windsurf, Cline, …) take the Option 3 JSON block in their o
135
135
  | `replace_in_note` | Find and replace text in the note body — literal or regex, whole-word, case sensitivity, optional `limit` (replaces **all** occurrences by default), dry-run preview. |
136
136
  | `append_periodic_note` | Append to today's periodic note, creating it from a template if it doesn't yet exist. |
137
137
 
138
- The content-editing tools (`append_note`, `patch_note`, `patch_frontmatter`, `replace_in_note`, `append_periodic_note`, and `create_note` with `overwrite: true`) support optional **compare-and-swap**: pass the `contentHash` you got from `read_note` as `prevHash` and the write fails cleanly if the note changed underneath you, instead of silently discarding the concurrent edit.
138
+ Every write tool (`append_note`, `patch_note`, `patch_frontmatter`, `replace_in_note`, `rename_heading`, `move_note`, `delete_note`, `append_periodic_note`, and `create_note` with `overwrite: true`) supports optional **compare-and-swap**: pass the `contentHash` you got from `read_note` as `prevHash` and the call fails cleanly if the note changed underneath you — no silently discarded concurrent edit, no moving or deleting content you haven't seen. Every mutating result returns the new `contentHash`, so chained edits need no re-reads.
139
139
 
140
140
  ---
141
141
 
package/dist/index.js CHANGED
@@ -701,7 +701,7 @@ ${stringifyYaml(input.frontmatter, { lineWidth: 0 })}---
701
701
  }
702
702
 
703
703
  // src/tools/delete_note.ts
704
- import { access as access2, mkdir as mkdir2, rename as rename2, rm } from "fs/promises";
704
+ import { access as access2, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm } from "fs/promises";
705
705
  import { basename as basename2, extname as extname2, join } from "path";
706
706
  import { z as z5 } from "zod";
707
707
 
@@ -742,6 +742,9 @@ var DeleteNoteInput = z5.object({
742
742
  path: z5.string().describe("Vault-relative path of the note to delete."),
743
743
  permanent: z5.boolean().default(false).describe(
744
744
  "Permanently remove the note instead of moving it to the vault .trash/ folder. Defaults to false \u2014 deletes are recoverable."
745
+ ),
746
+ prevHash: z5.string().optional().describe(
747
+ "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since \u2014 so you never delete content you have not seen."
745
748
  )
746
749
  });
747
750
  async function freeTrashPath(vaultRoot2, noteBase) {
@@ -761,11 +764,12 @@ async function deleteNote(ctx2, rawInput) {
761
764
  const input = DeleteNoteInput.parse(rawInput);
762
765
  const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
763
766
  assertWritable(ctx2.policy, input.path);
767
+ const raw = await readFile3(absPath, "utf8");
768
+ if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
764
769
  let trashedTo;
765
770
  if (input.permanent) {
766
771
  await rm(absPath);
767
772
  } else {
768
- await access2(absPath);
769
773
  await mkdir2(join(ctx2.vaultRoot, ".trash"), { recursive: true });
770
774
  trashedTo = await freeTrashPath(ctx2.vaultRoot, basename2(input.path));
771
775
  await rename2(absPath, join(ctx2.vaultRoot, trashedTo));
@@ -775,7 +779,11 @@ async function deleteNote(ctx2, rawInput) {
775
779
  ctx2.index.discard(input.path);
776
780
  ctx2.notes.delete(input.path);
777
781
  }
778
- const result = { path: input.path, permanent: input.permanent };
782
+ const result = {
783
+ path: input.path,
784
+ permanent: input.permanent,
785
+ contentHash: contentHash(raw)
786
+ };
779
787
  if (trashedTo !== void 0) result.trashedTo = trashedTo;
780
788
  return result;
781
789
  }
@@ -903,7 +911,7 @@ function listTags(ctx2, input) {
903
911
  }
904
912
 
905
913
  // src/tools/move_note.ts
906
- import { access as access3, mkdir as mkdir3, rename as rename3 } from "fs/promises";
914
+ import { access as access3, mkdir as mkdir3, readFile as readFile4, rename as rename3 } from "fs/promises";
907
915
  import { dirname as dirname2, join as join2 } from "path";
908
916
  import { z as z10 } from "zod";
909
917
 
@@ -1006,6 +1014,9 @@ var MoveNoteInput = z10.object({
1006
1014
  overwrite: z10.boolean().default(false).describe("Overwrite the destination if it exists. Defaults to false."),
1007
1015
  rewriteLinks: z10.boolean().default(true).describe(
1008
1016
  "Rewrite wikilinks and markdown links in other notes that point at the moved note, so nothing breaks. Defaults to true; pass false to move the file only."
1017
+ ),
1018
+ prevHash: z10.string().optional().describe(
1019
+ "Optional compare-and-swap guard on the source note: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
1009
1020
  )
1010
1021
  });
1011
1022
  async function moveNote(ctx2, rawInput) {
@@ -1014,11 +1025,13 @@ async function moveNote(ctx2, rawInput) {
1014
1025
  const absTo = resolveVaultPath(ctx2.vaultRoot, input.to);
1015
1026
  assertWritable(ctx2.policy, input.from);
1016
1027
  assertWritable(ctx2.policy, input.to);
1028
+ let raw;
1017
1029
  try {
1018
- await access3(absFrom);
1030
+ raw = await readFile4(absFrom, "utf8");
1019
1031
  } catch {
1020
1032
  throw new Error(`Note not found: ${input.from}`);
1021
1033
  }
1034
+ if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.from);
1022
1035
  if (!input.overwrite) {
1023
1036
  try {
1024
1037
  await access3(absTo);
@@ -1041,7 +1054,6 @@ async function moveNote(ctx2, rawInput) {
1041
1054
  }
1042
1055
  await mkdir3(dirname2(absTo), { recursive: true });
1043
1056
  await rename3(absFrom, absTo);
1044
- const raw = ctx2.notes.get(input.from)?.raw ?? "";
1045
1057
  removeNoteBacklinks(ctx2, input.from);
1046
1058
  if (ctx2.notes.has(input.from)) {
1047
1059
  ctx2.index.discard(input.from);
@@ -1079,13 +1091,19 @@ async function moveNote(ctx2, rawInput) {
1079
1091
  }
1080
1092
  }
1081
1093
  ctx2.backlinks.delete(input.from);
1082
- const result = { from: input.from, to: input.to, notesRewritten, linksRewritten };
1094
+ const result = {
1095
+ from: input.from,
1096
+ to: input.to,
1097
+ notesRewritten,
1098
+ linksRewritten,
1099
+ contentHash: contentHash(raw)
1100
+ };
1083
1101
  if (skipped.length > 0) result.skipped = skipped;
1084
1102
  return result;
1085
1103
  }
1086
1104
 
1087
1105
  // src/tools/outline_note.ts
1088
- import { readFile as readFile3 } from "fs/promises";
1106
+ import { readFile as readFile5 } from "fs/promises";
1089
1107
 
1090
1108
  // ../core/src/outline.ts
1091
1109
  var HEADING_RE = /^(#{1,6})\s+(.+)/;
@@ -1143,7 +1161,7 @@ var OutlineNoteInput = z11.object({
1143
1161
  });
1144
1162
  async function outlineNote(ctx2, input) {
1145
1163
  const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1146
- const raw = await readFile3(absPath, "utf8");
1164
+ const raw = await readFile5(absPath, "utf8");
1147
1165
  return buildOutline(raw, {
1148
1166
  includeBlocks: input.includeBlocks,
1149
1167
  includeSizes: input.includeSizes
@@ -1151,7 +1169,7 @@ async function outlineNote(ctx2, input) {
1151
1169
  }
1152
1170
 
1153
1171
  // src/tools/patch_frontmatter.ts
1154
- import { readFile as readFile4 } from "fs/promises";
1172
+ import { readFile as readFile6 } from "fs/promises";
1155
1173
  import { parseDocument as parseDocument2 } from "yaml";
1156
1174
  import { z as z12 } from "zod";
1157
1175
  var PatchFrontmatterInput = z12.object({
@@ -1166,7 +1184,7 @@ var PatchFrontmatterInput = z12.object({
1166
1184
  async function patchFrontmatter(ctx2, input) {
1167
1185
  const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1168
1186
  assertWritable(ctx2.policy, input.path);
1169
- const original = await readFile4(absPath, "utf8");
1187
+ const original = await readFile6(absPath, "utf8");
1170
1188
  if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
1171
1189
  const fm = parseFrontmatter(original);
1172
1190
  const keysChanged = [];
@@ -1226,7 +1244,7 @@ ${original}`;
1226
1244
  }
1227
1245
 
1228
1246
  // src/tools/patch_note.ts
1229
- import { readFile as readFile5 } from "fs/promises";
1247
+ import { readFile as readFile7 } from "fs/promises";
1230
1248
  import { z as z13 } from "zod";
1231
1249
  var PatchNoteInput = z13.object({
1232
1250
  path: z13.string().describe("Vault-relative path to the note."),
@@ -1257,7 +1275,7 @@ function lineStart(raw, offset) {
1257
1275
  async function patchNote(ctx2, input) {
1258
1276
  const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1259
1277
  assertWritable(ctx2.policy, input.path);
1260
- const raw = await readFile5(absPath, "utf8");
1278
+ const raw = await readFile7(absPath, "utf8");
1261
1279
  if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
1262
1280
  const fm = parseFrontmatter(raw);
1263
1281
  const originalFmRegion = raw.slice(0, fm.bodyStart);
@@ -1357,7 +1375,7 @@ ${input.content}${contentNl}`;
1357
1375
  };
1358
1376
  }
1359
1377
  async function verifyFrontmatter(absPath, originalFmRegion) {
1360
- const written = await readFile5(absPath, "utf8");
1378
+ const written = await readFile7(absPath, "utf8");
1361
1379
  if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
1362
1380
  throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
1363
1381
  }
@@ -1372,7 +1390,7 @@ function updateCache(ctx2, path2, newContent, bodyStart) {
1372
1390
  }
1373
1391
 
1374
1392
  // src/tools/periodic_note.ts
1375
- import { access as access4, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
1393
+ import { access as access4, mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
1376
1394
  import { dirname as dirname3, join as join3 } from "path";
1377
1395
  import { z as z14 } from "zod";
1378
1396
  var PERIOD = z14.enum(["daily", "weekly", "monthly", "quarterly", "yearly"]);
@@ -1385,7 +1403,7 @@ var DEFAULTS = {
1385
1403
  };
1386
1404
  async function readJson(absPath) {
1387
1405
  try {
1388
- return JSON.parse(await readFile6(absPath, "utf8"));
1406
+ return JSON.parse(await readFile8(absPath, "utf8"));
1389
1407
  } catch {
1390
1408
  return null;
1391
1409
  }
@@ -1471,7 +1489,7 @@ async function getPeriodicNote(ctx2, rawInput) {
1471
1489
  ctx2.vaultRoot,
1472
1490
  cfg.template.endsWith(".md") ? cfg.template : `${cfg.template}.md`
1473
1491
  );
1474
- body = await readFile6(templateAbs, "utf8");
1492
+ body = await readFile8(templateAbs, "utf8");
1475
1493
  } catch {
1476
1494
  }
1477
1495
  }
@@ -1498,7 +1516,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
1498
1516
  assertWritable(ctx2.policy, path2);
1499
1517
  let original = "";
1500
1518
  try {
1501
- original = await readFile6(abs, "utf8");
1519
+ original = await readFile8(abs, "utf8");
1502
1520
  if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, path2);
1503
1521
  } catch (err) {
1504
1522
  if (err.code !== "ENOENT") throw err;
@@ -1671,7 +1689,7 @@ function queryNotes(ctx2, input) {
1671
1689
  }
1672
1690
 
1673
1691
  // src/tools/read_note.ts
1674
- import { readFile as readFile7 } from "fs/promises";
1692
+ import { readFile as readFile9 } from "fs/promises";
1675
1693
  import { z as z16 } from "zod";
1676
1694
  var ReadNoteInput = z16.object({
1677
1695
  path: z16.string().describe('Vault-relative path to the note, e.g. "Daily Notes/2026-05-29.md".'),
@@ -1689,7 +1707,7 @@ var ReadNoteInput = z16.object({
1689
1707
  });
1690
1708
  async function readNote(ctx2, input) {
1691
1709
  const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1692
- const raw = await readFile7(absPath, "utf8");
1710
+ const raw = await readFile9(absPath, "utf8");
1693
1711
  const noteBytes = Buffer.byteLength(raw, "utf8");
1694
1712
  const hash = contentHash(raw);
1695
1713
  if (input.section === void 0 && input.block === void 0 && input.lines === void 0) {
@@ -1777,7 +1795,7 @@ async function readNote(ctx2, input) {
1777
1795
  }
1778
1796
 
1779
1797
  // src/tools/rename_heading.ts
1780
- import { readFile as readFile8 } from "fs/promises";
1798
+ import { readFile as readFile10 } from "fs/promises";
1781
1799
  import { join as join4 } from "path";
1782
1800
  import { z as z17 } from "zod";
1783
1801
  var RenameHeadingInput = z17.object({
@@ -1818,7 +1836,7 @@ async function renameHeading(ctx2, rawInput) {
1818
1836
  assertWritable(ctx2.policy, input.path);
1819
1837
  let raw;
1820
1838
  try {
1821
- raw = await readFile8(absPath, "utf8");
1839
+ raw = await readFile10(absPath, "utf8");
1822
1840
  } catch {
1823
1841
  throw new Error(`Note not found: ${input.path}`);
1824
1842
  }
@@ -1854,7 +1872,7 @@ async function renameHeading(ctx2, rawInput) {
1854
1872
  newRaw = newRaw.slice(0, fm.bodyStart) + selfRewrite.content;
1855
1873
  const originalFmRegion = raw.slice(0, fm.bodyStart);
1856
1874
  await atomicWrite(absPath, newRaw);
1857
- const written = await readFile8(absPath, "utf8");
1875
+ const written = await readFile10(absPath, "utf8");
1858
1876
  if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
1859
1877
  throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
1860
1878
  }
@@ -1900,7 +1918,7 @@ async function renameHeading(ctx2, rawInput) {
1900
1918
  }
1901
1919
 
1902
1920
  // src/tools/replace_in_note.ts
1903
- import { readFile as readFile9 } from "fs/promises";
1921
+ import { readFile as readFile11 } from "fs/promises";
1904
1922
  import { z as z18 } from "zod";
1905
1923
  var ReplaceInNoteInput = z18.object({
1906
1924
  path: z18.string().describe("Vault-relative path to the note."),
@@ -1937,7 +1955,7 @@ async function replaceInNote(ctx2, input) {
1937
1955
  } catch (err) {
1938
1956
  throw new Error(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`);
1939
1957
  }
1940
- const raw = await readFile9(absPath, "utf8");
1958
+ const raw = await readFile11(absPath, "utf8");
1941
1959
  if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
1942
1960
  const fm = parseFrontmatter(raw);
1943
1961
  const originalFmRegion = raw.slice(0, fm.bodyStart);
@@ -1954,7 +1972,10 @@ async function replaceInNote(ctx2, input) {
1954
1972
  path: input.path,
1955
1973
  replacements: toReplace.length,
1956
1974
  matches: matches2,
1957
- frontmatterUnchanged: true
1975
+ frontmatterUnchanged: true,
1976
+ // Nothing was written — hash the content as read, so chained edits can
1977
+ // proceed from a dry run without a re-read.
1978
+ contentHash: contentHash(raw)
1958
1979
  };
1959
1980
  }
1960
1981
  const singleRe = new RegExp(re.source, re.flags.replace("g", ""));
@@ -1969,7 +1990,7 @@ async function replaceInNote(ctx2, input) {
1969
1990
  newBody += body.slice(lastIndex);
1970
1991
  const newContent = `${originalFmRegion}${newBody}`;
1971
1992
  await atomicWrite(absPath, newContent);
1972
- const written = await readFile9(absPath, "utf8");
1993
+ const written = await readFile11(absPath, "utf8");
1973
1994
  if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
1974
1995
  throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
1975
1996
  }
@@ -2212,7 +2233,7 @@ async function run(ctx2, name, args) {
2212
2233
  }
2213
2234
 
2214
2235
  // src/index/build.ts
2215
- import { readFile as readFile10 } from "fs/promises";
2236
+ import { readFile as readFile12 } from "fs/promises";
2216
2237
  import { basename as basename3, extname as extname4 } from "path";
2217
2238
 
2218
2239
  // ../core/src/walk.ts
@@ -2294,7 +2315,7 @@ async function buildIndex(vaultRoot2) {
2294
2315
  await Promise.all(
2295
2316
  noteEntries.map(async (entry) => {
2296
2317
  try {
2297
- const raw = await readFile10(entry.absPath, "utf8");
2318
+ const raw = await readFile12(entry.absPath, "utf8");
2298
2319
  const fm = parseFrontmatter(raw);
2299
2320
  const inlineTags = extractInlineTags(fm.body);
2300
2321
  const fmTags = frontmatterTags(fm.data);
@@ -2347,7 +2368,7 @@ function buildBacklinks(notes2) {
2347
2368
 
2348
2369
  // src/init.ts
2349
2370
  import { execFileSync } from "child_process";
2350
- import { access as access5, copyFile, mkdir as mkdir5, readdir, readFile as readFile11, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2371
+ import { access as access5, copyFile, mkdir as mkdir5, readdir, readFile as readFile13, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2351
2372
  import path, { dirname as dirname4, join as join6 } from "path";
2352
2373
  function parseInitArgs(argv) {
2353
2374
  const opts = { write: false, client: "desktop" };
@@ -2491,7 +2512,7 @@ async function runInit(opts, deps) {
2491
2512
  ]
2492
2513
  };
2493
2514
  }
2494
- const rf = deps.readFile ?? readFile11;
2515
+ const rf = deps.readFile ?? readFile13;
2495
2516
  let vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT;
2496
2517
  if (!vaultPath) {
2497
2518
  const registryPath = obsidianRegistryPath(deps.platform, {
@@ -2981,6 +3002,10 @@ var ALL_TOOLS = [
2981
3002
  permanent: {
2982
3003
  type: "boolean",
2983
3004
  description: "Permanently remove instead of moving to .trash/. Defaults to false."
3005
+ },
3006
+ prevHash: {
3007
+ type: "string",
3008
+ description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since \u2014 so you never delete content you have not seen."
2984
3009
  }
2985
3010
  },
2986
3011
  required: ["path"]
@@ -3002,6 +3027,10 @@ var ALL_TOOLS = [
3002
3027
  rewriteLinks: {
3003
3028
  type: "boolean",
3004
3029
  description: "Rewrite references in other notes to follow the move. Defaults to true; pass false to move the file only."
3030
+ },
3031
+ prevHash: {
3032
+ type: "string",
3033
+ description: "Optional compare-and-swap guard on the source note: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
3005
3034
  }
3006
3035
  },
3007
3036
  required: ["from", "to"]
@@ -3259,7 +3288,7 @@ function visibleTools(policy2) {
3259
3288
 
3260
3289
  // src/watcher.ts
3261
3290
  import { realpathSync } from "fs";
3262
- import { readFile as readFile12 } from "fs/promises";
3291
+ import { readFile as readFile14 } from "fs/promises";
3263
3292
  import { join as join7, relative as relative2, sep as sep3 } from "path";
3264
3293
  import chokidar from "chokidar";
3265
3294
  function startWatcher(ctx2, log2, opts) {
@@ -3272,7 +3301,7 @@ function startWatcher(ctx2, log2, opts) {
3272
3301
  const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
3273
3302
  async function upsert(relPath, op) {
3274
3303
  try {
3275
- const raw = await readFile12(join7(vaultRoot2, relPath), "utf8");
3304
+ const raw = await readFile14(join7(vaultRoot2, relPath), "utf8");
3276
3305
  removeNoteBacklinks(ctx2, relPath);
3277
3306
  upsertDoc(ctx2, buildDoc(relPath, raw));
3278
3307
  addNoteBacklinks(ctx2, relPath, raw);
@@ -3324,7 +3353,7 @@ function startWatcher(ctx2, log2, opts) {
3324
3353
  }
3325
3354
 
3326
3355
  // src/index.ts
3327
- var VERSION = true ? "0.13.1" : "0.0.0-dev";
3356
+ var VERSION = true ? "0.14.0" : "0.0.0-dev";
3328
3357
  var intent = parseCliIntent(process.argv.slice(2));
3329
3358
  if (intent === "version") {
3330
3359
  process.stdout.write(`${VERSION}