seekstone 0.13.0 → 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, {
@@ -2752,9 +2773,32 @@ function installProcessGuards(log2) {
2752
2773
  }
2753
2774
 
2754
2775
  // src/tool-list.ts
2776
+ var READ_ONLY_ANNOTATIONS = {
2777
+ readOnlyHint: true,
2778
+ openWorldHint: false
2779
+ };
2780
+ var ADDITIVE_WRITE_ANNOTATIONS = {
2781
+ readOnlyHint: false,
2782
+ destructiveHint: false,
2783
+ idempotentHint: false,
2784
+ openWorldHint: false
2785
+ };
2786
+ var DESTRUCTIVE_WRITE_ANNOTATIONS = {
2787
+ readOnlyHint: false,
2788
+ destructiveHint: true,
2789
+ idempotentHint: false,
2790
+ openWorldHint: false
2791
+ };
2792
+ var IDEMPOTENT_DESTRUCTIVE_WRITE_ANNOTATIONS = {
2793
+ readOnlyHint: false,
2794
+ destructiveHint: true,
2795
+ idempotentHint: true,
2796
+ openWorldHint: false
2797
+ };
2755
2798
  var ALL_TOOLS = [
2756
2799
  {
2757
2800
  name: "search",
2801
+ annotations: READ_ONLY_ANNOTATIONS,
2758
2802
  description: "Full-text search across the vault. Returns ranked excerpts (~200 chars) \u2014 not full notes \u2014 to minimise context usage. Supports fuzzy matching and prefix search.",
2759
2803
  inputSchema: {
2760
2804
  type: "object",
@@ -2769,6 +2813,7 @@ var ALL_TOOLS = [
2769
2813
  },
2770
2814
  {
2771
2815
  name: "query_notes",
2816
+ annotations: READ_ONLY_ANNOTATIONS,
2772
2817
  description: "Structured metadata query \u2014 filter notes by frontmatter key/value predicates, tag, folder, modified time, and size. Returns compact rows (path + title by default; opt into more via select), not note content. Use this instead of search when filtering by properties rather than text.",
2773
2818
  inputSchema: {
2774
2819
  type: "object",
@@ -2826,6 +2871,7 @@ var ALL_TOOLS = [
2826
2871
  },
2827
2872
  {
2828
2873
  name: "context_pack",
2874
+ annotations: READ_ONLY_ANNOTATIONS,
2829
2875
  description: 'Assemble everything needed to ANSWER a natural-language question in one call, under a strict byte budget (default 2048): ranked excerpts, linked neighbor notes (backlinks/outlinks) with one-line summaries, and follow-up source paths. Use search to locate notes and query_notes for metadata filters; use context_pack when you want answer-ready context without multiple round-trips. Empty excerpts with confidence "none" or "low" means the vault lacks coverage \u2014 do not infer content.',
2830
2876
  inputSchema: {
2831
2877
  type: "object",
@@ -2841,6 +2887,7 @@ var ALL_TOOLS = [
2841
2887
  },
2842
2888
  {
2843
2889
  name: "read_note",
2890
+ annotations: READ_ONLY_ANNOTATIONS,
2844
2891
  description: "Read a note or a span of it \u2014 by heading section, block reference, or line range. Returns structured JSON with the content, bytes returned, total note size, and a contentHash to pass as prevHash to edit tools for compare-and-swap. Use search or outline_note first to find the right path and section names.",
2845
2892
  inputSchema: {
2846
2893
  type: "object",
@@ -2879,6 +2926,7 @@ var ALL_TOOLS = [
2879
2926
  },
2880
2927
  {
2881
2928
  name: "list_notes",
2929
+ annotations: READ_ONLY_ANNOTATIONS,
2882
2930
  description: "List notes, optionally filtered by folder prefix or tag.",
2883
2931
  inputSchema: {
2884
2932
  type: "object",
@@ -2892,6 +2940,7 @@ var ALL_TOOLS = [
2892
2940
  },
2893
2941
  {
2894
2942
  name: "list_tags",
2943
+ annotations: READ_ONLY_ANNOTATIONS,
2895
2944
  description: "List all tags in the vault with usage counts. Supports substring filtering, minimum count threshold, and sort order. Nested tags (e.g. area/work) include a parent field.",
2896
2945
  inputSchema: {
2897
2946
  type: "object",
@@ -2915,6 +2964,7 @@ var ALL_TOOLS = [
2915
2964
  },
2916
2965
  {
2917
2966
  name: "create_note",
2967
+ annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
2918
2968
  description: "Create a new note at a vault-relative path. Optionally sets frontmatter and body content. Parent directories are created automatically. Fails if the note already exists unless overwrite is true (prevHash may guard the overwrite).",
2919
2969
  inputSchema: {
2920
2970
  type: "object",
@@ -2943,6 +2993,7 @@ var ALL_TOOLS = [
2943
2993
  },
2944
2994
  {
2945
2995
  name: "delete_note",
2996
+ annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
2946
2997
  description: "Delete a note. By default it is moved to the vault .trash/ folder (recoverable by moving it back); pass permanent: true to remove it outright.",
2947
2998
  inputSchema: {
2948
2999
  type: "object",
@@ -2951,6 +3002,10 @@ var ALL_TOOLS = [
2951
3002
  permanent: {
2952
3003
  type: "boolean",
2953
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."
2954
3009
  }
2955
3010
  },
2956
3011
  required: ["path"]
@@ -2958,6 +3013,7 @@ var ALL_TOOLS = [
2958
3013
  },
2959
3014
  {
2960
3015
  name: "move_note",
3016
+ annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
2961
3017
  description: "Move or rename a note to a new vault-relative path, rewriting wikilinks and markdown links in other notes that point at it so nothing breaks (links inside fenced code blocks are left alone). Parent directories at the destination are created automatically. Fails if the destination already exists unless overwrite is true.",
2962
3018
  inputSchema: {
2963
3019
  type: "object",
@@ -2971,6 +3027,10 @@ var ALL_TOOLS = [
2971
3027
  rewriteLinks: {
2972
3028
  type: "boolean",
2973
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."
2974
3034
  }
2975
3035
  },
2976
3036
  required: ["from", "to"]
@@ -2978,6 +3038,7 @@ var ALL_TOOLS = [
2978
3038
  },
2979
3039
  {
2980
3040
  name: "rename_heading",
3041
+ annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
2981
3042
  description: "Rename a heading in a note and rewrite every [[note#heading]] wikilink and embed across the vault so references keep working \u2014 aliases preserved, fenced code blocks left alone. Served from the warm backlink index, no vault scan. Heading matching is case-insensitive; with duplicate headings the first match wins, mirroring Obsidian link resolution.",
2982
3043
  inputSchema: {
2983
3044
  type: "object",
@@ -3004,6 +3065,7 @@ var ALL_TOOLS = [
3004
3065
  },
3005
3066
  {
3006
3067
  name: "append_note",
3068
+ annotations: ADDITIVE_WRITE_ANNOTATIONS,
3007
3069
  description: "Append text to a note body without touching the frontmatter. Safe for meeting notes, daily logs, and append-only workflows.",
3008
3070
  inputSchema: {
3009
3071
  type: "object",
@@ -3020,6 +3082,7 @@ var ALL_TOOLS = [
3020
3082
  },
3021
3083
  {
3022
3084
  name: "patch_frontmatter",
3085
+ annotations: IDEMPOTENT_DESTRUCTIVE_WRITE_ANNOTATIONS,
3023
3086
  description: "Set, update, or delete frontmatter keys without reordering existing keys or changing quote style. Pass null as a value to delete a key.",
3024
3087
  inputSchema: {
3025
3088
  type: "object",
@@ -3040,6 +3103,7 @@ var ALL_TOOLS = [
3040
3103
  },
3041
3104
  {
3042
3105
  name: "outline_note",
3106
+ annotations: READ_ONLY_ANNOTATIONS,
3043
3107
  description: "Return a note's structure \u2014 heading tree with offsets, block-reference anchors, and frontmatter key list \u2014 without returning any prose. Use this before section reads or patches to discover what sections exist at a fraction of the cost of reading the full note.",
3044
3108
  inputSchema: {
3045
3109
  type: "object",
@@ -3059,6 +3123,7 @@ var ALL_TOOLS = [
3059
3123
  },
3060
3124
  {
3061
3125
  name: "patch_note",
3126
+ annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
3062
3127
  description: "Surgically edit a section of a note \u2014 targeted by heading or block reference \u2014 without rewriting the whole file. Operations: append (add after section), prepend (add after heading line), replace (swap section content). Frontmatter is never touched.",
3063
3128
  inputSchema: {
3064
3129
  type: "object",
@@ -3088,6 +3153,7 @@ var ALL_TOOLS = [
3088
3153
  },
3089
3154
  {
3090
3155
  name: "get_backlinks",
3156
+ annotations: READ_ONLY_ANNOTATIONS,
3091
3157
  description: "Return every note that links to the target note, with the source line and an optional excerpt. Results come from the pre-built reverse-link index so this is a fast, pure index lookup. Sort order: source path ascending.",
3092
3158
  inputSchema: {
3093
3159
  type: "object",
@@ -3107,6 +3173,7 @@ var ALL_TOOLS = [
3107
3173
  },
3108
3174
  {
3109
3175
  name: "get_links",
3176
+ annotations: READ_ONLY_ANNOTATIONS,
3110
3177
  description: "Return all outgoing wikilinks and embeds from a note. Each link is marked resolved (with target path) or unresolved. Duplicate targets are de-duplicated; results sorted by line number.",
3111
3178
  inputSchema: {
3112
3179
  type: "object",
@@ -3118,6 +3185,7 @@ var ALL_TOOLS = [
3118
3185
  },
3119
3186
  {
3120
3187
  name: "replace_in_note",
3188
+ annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
3121
3189
  description: "Find and replace text within a note body. Supports literal and regex search, case sensitivity, whole-word matching, and a replacement limit. Frontmatter is never touched. Use dryRun to preview matches before writing.",
3122
3190
  inputSchema: {
3123
3191
  type: "object",
@@ -3158,6 +3226,7 @@ var ALL_TOOLS = [
3158
3226
  },
3159
3227
  {
3160
3228
  name: "get_periodic_note",
3229
+ annotations: ADDITIVE_WRITE_ANNOTATIONS,
3161
3230
  description: "Get the path and existence status of a periodic note (daily, weekly, monthly, quarterly, or yearly) for a given date. Reads folder/format config from .obsidian/daily-notes.json (daily) or the periodic-notes plugin data.json. Optionally creates the note from the configured template if it is missing.",
3162
3231
  inputSchema: {
3163
3232
  type: "object",
@@ -3181,6 +3250,7 @@ var ALL_TOOLS = [
3181
3250
  },
3182
3251
  {
3183
3252
  name: "append_periodic_note",
3253
+ annotations: ADDITIVE_WRITE_ANNOTATIONS,
3184
3254
  description: "Append text to a periodic note (daily, weekly, monthly, quarterly, or yearly). Preserves existing frontmatter exactly. Creates the note first (from template if configured) when createIfMissing is true (default).",
3185
3255
  inputSchema: {
3186
3256
  type: "object",
@@ -3218,7 +3288,7 @@ function visibleTools(policy2) {
3218
3288
 
3219
3289
  // src/watcher.ts
3220
3290
  import { realpathSync } from "fs";
3221
- import { readFile as readFile12 } from "fs/promises";
3291
+ import { readFile as readFile14 } from "fs/promises";
3222
3292
  import { join as join7, relative as relative2, sep as sep3 } from "path";
3223
3293
  import chokidar from "chokidar";
3224
3294
  function startWatcher(ctx2, log2, opts) {
@@ -3231,7 +3301,7 @@ function startWatcher(ctx2, log2, opts) {
3231
3301
  const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
3232
3302
  async function upsert(relPath, op) {
3233
3303
  try {
3234
- const raw = await readFile12(join7(vaultRoot2, relPath), "utf8");
3304
+ const raw = await readFile14(join7(vaultRoot2, relPath), "utf8");
3235
3305
  removeNoteBacklinks(ctx2, relPath);
3236
3306
  upsertDoc(ctx2, buildDoc(relPath, raw));
3237
3307
  addNoteBacklinks(ctx2, relPath, raw);
@@ -3283,7 +3353,7 @@ function startWatcher(ctx2, log2, opts) {
3283
3353
  }
3284
3354
 
3285
3355
  // src/index.ts
3286
- var VERSION = true ? "0.13.0" : "0.0.0-dev";
3356
+ var VERSION = true ? "0.14.0" : "0.0.0-dev";
3287
3357
  var intent = parseCliIntent(process.argv.slice(2));
3288
3358
  if (intent === "version") {
3289
3359
  process.stdout.write(`${VERSION}