seekstone 0.12.1 → 0.13.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/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { resolve as resolve2 } from "path";
4
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
7
  import {
@@ -75,7 +76,6 @@ Docs: https://github.com/shaqmughal/seekstone`;
75
76
 
76
77
  // src/tools/append_note.ts
77
78
  import { readFile } from "fs/promises";
78
- import { join } from "path";
79
79
 
80
80
  // ../core/src/frontmatter.ts
81
81
  import { parse as parseYaml } from "yaml";
@@ -200,6 +200,16 @@ function assertWritable(policy2, relPath) {
200
200
  }
201
201
  }
202
202
 
203
+ // src/vault-path.ts
204
+ import { resolve, sep } from "path";
205
+ function resolveVaultPath(vaultRoot2, inputPath) {
206
+ const abs = resolve(vaultRoot2, inputPath);
207
+ if (abs !== vaultRoot2 && !abs.startsWith(vaultRoot2 + sep)) {
208
+ throw new Error(`Path outside vault: ${inputPath}`);
209
+ }
210
+ return abs;
211
+ }
212
+
203
213
  // src/tools/append_note.ts
204
214
  var AppendNoteInput = z.object({
205
215
  path: z.string().describe("Vault-relative path to the note."),
@@ -211,10 +221,7 @@ var AppendNoteInput = z.object({
211
221
  )
212
222
  });
213
223
  async function appendNote(ctx2, input) {
214
- const absPath = join(ctx2.vaultRoot, input.path);
215
- if (!absPath.startsWith(ctx2.vaultRoot)) {
216
- throw new Error(`Path outside vault: ${input.path}`);
217
- }
224
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
218
225
  assertWritable(ctx2.policy, input.path);
219
226
  const original = await readFile(absPath, "utf8");
220
227
  if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
@@ -239,25 +246,64 @@ async function appendNote(ctx2, input) {
239
246
  }
240
247
 
241
248
  // ../core/src/extract.ts
249
+ import { parseDocument, visit } from "yaml";
242
250
  var LINK_WITH_LINES_RE = /(!?\[\[)([^\]|#\n]{1,512})(#[^\]|\n]{1,512})?(\|[^\]\n]{1,512})?(\]\])/g;
243
251
  function extractLinksWithLines(raw) {
252
+ const fm = parseFrontmatter(raw);
253
+ if (!fm.present || fm.bodyStart === 0) return linksFromLines(raw, 1);
254
+ const fmRegion = raw.slice(0, fm.bodyStart);
255
+ const out = frontmatterLinks(fmRegion, fm.malformed);
256
+ const bodyFirstLine = countNewlines(fmRegion) + 1;
257
+ linksFromLinesInto(out, fm.body, bodyFirstLine);
258
+ return out;
259
+ }
260
+ function linksFromLines(text, firstLine) {
244
261
  const out = [];
245
- const lines = raw.split("\n");
262
+ linksFromLinesInto(out, text, firstLine);
263
+ return out;
264
+ }
265
+ function linksFromLinesInto(out, text, firstLine) {
266
+ const lines = text.split("\n");
246
267
  for (const [lineIdx, lineText] of lines.entries()) {
247
- for (const m of lineText.matchAll(LINK_WITH_LINES_RE)) {
248
- const prefix = m[1] ?? "";
249
- out.push({
250
- raw: m[0] ?? "",
251
- target: (m[2] ?? "").trim(),
252
- fragment: m[3] ? m[3].slice(1).trim() : null,
253
- alias: m[4] ? m[4].slice(1).trim() : null,
254
- linkType: prefix.startsWith("!") ? "embed" : "wikilink",
255
- line: lineIdx + 1
256
- });
257
- }
268
+ matchLinksInto(out, lineText, firstLine + lineIdx);
269
+ }
270
+ }
271
+ function matchLinksInto(out, text, line) {
272
+ for (const m of text.matchAll(LINK_WITH_LINES_RE)) {
273
+ const prefix = m[1] ?? "";
274
+ out.push({
275
+ raw: m[0] ?? "",
276
+ target: (m[2] ?? "").trim(),
277
+ fragment: m[3] ? m[3].slice(1).trim() : null,
278
+ alias: m[4] ? m[4].slice(1).trim() : null,
279
+ linkType: prefix.startsWith("!") ? "embed" : "wikilink",
280
+ line
281
+ });
258
282
  }
283
+ }
284
+ function frontmatterLinks(fmRegion, malformed) {
285
+ if (malformed) return linksFromLines(fmRegion, 1);
286
+ const yamlStart = fmRegion.indexOf("\n") + 1;
287
+ const closeLen = fmRegion.endsWith("---\r\n") ? 5 : 4;
288
+ const yamlText = fmRegion.slice(yamlStart, fmRegion.length - closeLen);
289
+ const doc = parseDocument(yamlText);
290
+ if (doc.errors.length > 0) return linksFromLines(fmRegion, 1);
291
+ const out = [];
292
+ visit(doc, {
293
+ Scalar(key, node) {
294
+ if (key === "key") return;
295
+ if (typeof node.value !== "string" || node.range == null) return;
296
+ const line = 2 + countNewlines(yamlText.slice(0, node.range[0]));
297
+ matchLinksInto(out, node.value, line);
298
+ }
299
+ });
259
300
  return out;
260
301
  }
302
+ function countNewlines(text) {
303
+ let n = 0;
304
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) n++;
305
+ return n;
306
+ }
261
307
  var INLINE_TAG_RE = /(?:^|[^\w\]/])#([A-Za-z_][\w/-]*)/g;
262
308
  function extractInlineTags(body) {
263
309
  const out = [];
@@ -565,7 +611,7 @@ function contextPack(ctx2, input) {
565
611
 
566
612
  // src/tools/create_note.ts
567
613
  import { access, mkdir, readFile as readFile2 } from "fs/promises";
568
- import { dirname, join as join2 } from "path";
614
+ import { dirname } from "path";
569
615
  import { stringify as stringifyYaml } from "yaml";
570
616
  import { z as z4 } from "zod";
571
617
 
@@ -611,10 +657,7 @@ var CreateNoteInput = z4.object({
611
657
  });
612
658
  async function createNote(ctx2, rawInput) {
613
659
  const input = CreateNoteInput.parse(rawInput);
614
- const absPath = join2(ctx2.vaultRoot, input.path);
615
- if (!absPath.startsWith(ctx2.vaultRoot)) {
616
- throw new Error(`Path outside vault: ${input.path}`);
617
- }
660
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
618
661
  assertWritable(ctx2.policy, input.path);
619
662
  if (!input.overwrite) {
620
663
  try {
@@ -659,7 +702,7 @@ ${stringifyYaml(input.frontmatter, { lineWidth: 0 })}---
659
702
 
660
703
  // src/tools/delete_note.ts
661
704
  import { access as access2, mkdir as mkdir2, rename as rename2, rm } from "fs/promises";
662
- import { basename as basename2, extname as extname2, join as join3 } from "path";
705
+ import { basename as basename2, extname as extname2, join } from "path";
663
706
  import { z as z5 } from "zod";
664
707
 
665
708
  // src/index/backlinks.ts
@@ -707,7 +750,7 @@ async function freeTrashPath(vaultRoot2, noteBase) {
707
750
  let candidate = `.trash/${noteBase}`;
708
751
  for (; ; ) {
709
752
  try {
710
- await access2(join3(vaultRoot2, candidate));
753
+ await access2(join(vaultRoot2, candidate));
711
754
  } catch {
712
755
  return candidate;
713
756
  }
@@ -716,19 +759,16 @@ async function freeTrashPath(vaultRoot2, noteBase) {
716
759
  }
717
760
  async function deleteNote(ctx2, rawInput) {
718
761
  const input = DeleteNoteInput.parse(rawInput);
719
- const absPath = join3(ctx2.vaultRoot, input.path);
720
- if (!absPath.startsWith(ctx2.vaultRoot)) {
721
- throw new Error(`Path outside vault: ${input.path}`);
722
- }
762
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
723
763
  assertWritable(ctx2.policy, input.path);
724
764
  let trashedTo;
725
765
  if (input.permanent) {
726
766
  await rm(absPath);
727
767
  } else {
728
768
  await access2(absPath);
729
- await mkdir2(join3(ctx2.vaultRoot, ".trash"), { recursive: true });
769
+ await mkdir2(join(ctx2.vaultRoot, ".trash"), { recursive: true });
730
770
  trashedTo = await freeTrashPath(ctx2.vaultRoot, basename2(input.path));
731
- await rename2(absPath, join3(ctx2.vaultRoot, trashedTo));
771
+ await rename2(absPath, join(ctx2.vaultRoot, trashedTo));
732
772
  }
733
773
  removeNoteBacklinks(ctx2, input.path);
734
774
  if (ctx2.notes.has(input.path)) {
@@ -741,7 +781,6 @@ async function deleteNote(ctx2, rawInput) {
741
781
  }
742
782
 
743
783
  // src/tools/get_backlinks.ts
744
- import { join as join4 } from "path";
745
784
  import { z as z6 } from "zod";
746
785
  var GetBacklinksInput = z6.object({
747
786
  path: z6.string().describe("Vault-relative path to the target note."),
@@ -750,10 +789,7 @@ var GetBacklinksInput = z6.object({
750
789
  });
751
790
  var EXCERPT_MAX = 200;
752
791
  function getBacklinks(ctx2, input) {
753
- const absPath = join4(ctx2.vaultRoot, input.path);
754
- if (!absPath.startsWith(ctx2.vaultRoot)) {
755
- throw new Error(`Path outside vault: ${input.path}`);
756
- }
792
+ resolveVaultPath(ctx2.vaultRoot, input.path);
757
793
  const includeContext = input.includeContext ?? true;
758
794
  const limit = input.limit ?? 50;
759
795
  const refs = ctx2.backlinks.get(input.path) ?? [];
@@ -778,16 +814,12 @@ function getBacklinks(ctx2, input) {
778
814
  }
779
815
 
780
816
  // src/tools/get_links.ts
781
- import { join as join5 } from "path";
782
817
  import { z as z7 } from "zod";
783
818
  var GetLinksInput = z7.object({
784
819
  path: z7.string().describe("Vault-relative path to the note.")
785
820
  });
786
821
  function getLinks(ctx2, input) {
787
- const absPath = join5(ctx2.vaultRoot, input.path);
788
- if (!absPath.startsWith(ctx2.vaultRoot)) {
789
- throw new Error(`Path outside vault: ${input.path}`);
790
- }
822
+ resolveVaultPath(ctx2.vaultRoot, input.path);
791
823
  const note = ctx2.notes.get(input.path);
792
824
  if (note === void 0) throw new Error(`Note not found: ${input.path}`);
793
825
  const rawLinks = extractLinksWithLines(note.raw);
@@ -872,7 +904,7 @@ function listTags(ctx2, input) {
872
904
 
873
905
  // src/tools/move_note.ts
874
906
  import { access as access3, mkdir as mkdir3, rename as rename3 } from "fs/promises";
875
- import { dirname as dirname2, join as join6 } from "path";
907
+ import { dirname as dirname2, join as join2 } from "path";
876
908
  import { z as z10 } from "zod";
877
909
 
878
910
  // src/tools/rewrite_links.ts
@@ -880,6 +912,16 @@ import { posix } from "path";
880
912
  var WIKI_RE = /(!?)\[\[([^\]|#\n]{1,512})(#[^\]|\n]{1,512})?(\|[^\]\n]{1,512})?\]\]/g;
881
913
  var MD_RE = /(!?)\[([^\]\n]*)\]\(([^)\n]+)\)/g;
882
914
  var FENCE_RE = /^\s*(```|~~~)/;
915
+ function mapUnfencedLines(raw, fn) {
916
+ let inFence = false;
917
+ return raw.split("\n").map((line) => {
918
+ if (FENCE_RE.test(line)) {
919
+ inFence = !inFence;
920
+ return line;
921
+ }
922
+ return inFence ? line : fn(line);
923
+ }).join("\n");
924
+ }
883
925
  function wikiTargetFor(newPath, postNotes) {
884
926
  const noExt = newPath.replace(/\.md$/i, "");
885
927
  const base = noExt.split("/").pop() ?? noExt;
@@ -939,16 +981,8 @@ function rewriteNoteLinks(raw, sourcePath, oldPath, newPath, preNotes, postNotes
939
981
  const target = needsWrap ? `<${next}${fragment}>` : `${next}${fragment}`;
940
982
  return `${bang}[${text}](${target})`;
941
983
  });
942
- let inFence = false;
943
- const lines = raw.split("\n").map((line) => {
944
- if (FENCE_RE.test(line)) {
945
- inFence = !inFence;
946
- return line;
947
- }
948
- if (inFence) return line;
949
- return rewriteMdLine(rewriteWikiLine(line));
950
- });
951
- return { content: lines.join("\n"), count };
984
+ const content = mapUnfencedLines(raw, (line) => rewriteMdLine(rewriteWikiLine(line)));
985
+ return { content, count };
952
986
  }
953
987
  function hasMarkdownLinkTo(raw, sourcePath, targetPath) {
954
988
  if (!raw.includes("](")) return false;
@@ -976,10 +1010,8 @@ var MoveNoteInput = z10.object({
976
1010
  });
977
1011
  async function moveNote(ctx2, rawInput) {
978
1012
  const input = MoveNoteInput.parse(rawInput);
979
- const absFrom = join6(ctx2.vaultRoot, input.from);
980
- const absTo = join6(ctx2.vaultRoot, input.to);
981
- if (!absFrom.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${input.from}`);
982
- if (!absTo.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${input.to}`);
1013
+ const absFrom = resolveVaultPath(ctx2.vaultRoot, input.from);
1014
+ const absTo = resolveVaultPath(ctx2.vaultRoot, input.to);
983
1015
  assertWritable(ctx2.policy, input.from);
984
1016
  assertWritable(ctx2.policy, input.to);
985
1017
  try {
@@ -1037,7 +1069,7 @@ async function moveNote(ctx2, rawInput) {
1037
1069
  );
1038
1070
  removeNoteBacklinks(ctx2, path2);
1039
1071
  if (count > 0) {
1040
- await atomicWrite(join6(ctx2.vaultRoot, path2), content);
1072
+ await atomicWrite(join2(ctx2.vaultRoot, path2), content);
1041
1073
  upsertDoc(ctx2, buildDoc(path2, content));
1042
1074
  notesRewritten++;
1043
1075
  linksRewritten += count;
@@ -1054,7 +1086,6 @@ async function moveNote(ctx2, rawInput) {
1054
1086
 
1055
1087
  // src/tools/outline_note.ts
1056
1088
  import { readFile as readFile3 } from "fs/promises";
1057
- import { join as join7 } from "path";
1058
1089
 
1059
1090
  // ../core/src/outline.ts
1060
1091
  var HEADING_RE = /^(#{1,6})\s+(.+)/;
@@ -1111,10 +1142,7 @@ var OutlineNoteInput = z11.object({
1111
1142
  includeSizes: z11.boolean().default(false).describe("Include per-section character length in each heading entry. Default false.")
1112
1143
  });
1113
1144
  async function outlineNote(ctx2, input) {
1114
- const absPath = join7(ctx2.vaultRoot, input.path);
1115
- if (!absPath.startsWith(ctx2.vaultRoot)) {
1116
- throw new Error(`Path outside vault: ${input.path}`);
1117
- }
1145
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1118
1146
  const raw = await readFile3(absPath, "utf8");
1119
1147
  return buildOutline(raw, {
1120
1148
  includeBlocks: input.includeBlocks,
@@ -1124,8 +1152,7 @@ async function outlineNote(ctx2, input) {
1124
1152
 
1125
1153
  // src/tools/patch_frontmatter.ts
1126
1154
  import { readFile as readFile4 } from "fs/promises";
1127
- import { join as join8 } from "path";
1128
- import { parseDocument } from "yaml";
1155
+ import { parseDocument as parseDocument2 } from "yaml";
1129
1156
  import { z as z12 } from "zod";
1130
1157
  var PatchFrontmatterInput = z12.object({
1131
1158
  path: z12.string().describe("Vault-relative path to the note."),
@@ -1137,10 +1164,7 @@ var PatchFrontmatterInput = z12.object({
1137
1164
  )
1138
1165
  });
1139
1166
  async function patchFrontmatter(ctx2, input) {
1140
- const absPath = join8(ctx2.vaultRoot, input.path);
1141
- if (!absPath.startsWith(ctx2.vaultRoot)) {
1142
- throw new Error(`Path outside vault: ${input.path}`);
1143
- }
1167
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1144
1168
  assertWritable(ctx2.policy, input.path);
1145
1169
  const original = await readFile4(absPath, "utf8");
1146
1170
  if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
@@ -1150,7 +1174,7 @@ async function patchFrontmatter(ctx2, input) {
1150
1174
  const keysRemoved = [];
1151
1175
  let newContent;
1152
1176
  if (!fm.present) {
1153
- const doc = parseDocument("");
1177
+ const doc = parseDocument2("");
1154
1178
  for (const [k, v] of Object.entries(input.patch)) {
1155
1179
  if (v !== null) {
1156
1180
  doc.set(k, v);
@@ -1165,7 +1189,7 @@ ${original}`;
1165
1189
  const openLen = opensWithCRLF ? 5 : 4;
1166
1190
  const closeLen = opensWithCRLF ? 7 : 5;
1167
1191
  const yamlText = original.slice(openLen, fm.bodyStart - closeLen);
1168
- const doc = parseDocument(yamlText, { keepSourceTokens: true });
1192
+ const doc = parseDocument2(yamlText, { keepSourceTokens: true });
1169
1193
  for (const [k, v] of Object.entries(input.patch)) {
1170
1194
  const existing = doc.has(k);
1171
1195
  if (v === null) {
@@ -1203,7 +1227,6 @@ ${original}`;
1203
1227
 
1204
1228
  // src/tools/patch_note.ts
1205
1229
  import { readFile as readFile5 } from "fs/promises";
1206
- import { join as join9 } from "path";
1207
1230
  import { z as z13 } from "zod";
1208
1231
  var PatchNoteInput = z13.object({
1209
1232
  path: z13.string().describe("Vault-relative path to the note."),
@@ -1232,10 +1255,7 @@ function lineStart(raw, offset) {
1232
1255
  return prev === -1 ? 0 : prev + 1;
1233
1256
  }
1234
1257
  async function patchNote(ctx2, input) {
1235
- const absPath = join9(ctx2.vaultRoot, input.path);
1236
- if (!absPath.startsWith(ctx2.vaultRoot)) {
1237
- throw new Error(`Path outside vault: ${input.path}`);
1238
- }
1258
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1239
1259
  assertWritable(ctx2.policy, input.path);
1240
1260
  const raw = await readFile5(absPath, "utf8");
1241
1261
  if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
@@ -1250,15 +1270,15 @@ async function patchNote(ctx2, input) {
1250
1270
  const heading = outline.headings.find((h) => h.text.toLowerCase() === targetText.toLowerCase());
1251
1271
  if (!heading) {
1252
1272
  if (input.createIfMissing) {
1253
- const sep3 = raw.endsWith("\n\n") ? "" : raw.endsWith("\n") ? "\n" : "\n\n";
1273
+ const sep4 = raw.endsWith("\n\n") ? "" : raw.endsWith("\n") ? "\n" : "\n\n";
1254
1274
  const contentNl = input.content.endsWith("\n") ? "" : "\n";
1255
- const newContent2 = `${raw}${sep3}## ${targetText}
1275
+ const newContent2 = `${raw}${sep4}## ${targetText}
1256
1276
 
1257
1277
  ${input.content}${contentNl}`;
1258
1278
  await atomicWrite(absPath, newContent2);
1259
1279
  await verifyFrontmatter(absPath, originalFmRegion);
1260
1280
  updateCache(ctx2, input.path, newContent2, fm.bodyStart);
1261
- const headingOffset = raw.length + sep3.length;
1281
+ const headingOffset = raw.length + sep4.length;
1262
1282
  return {
1263
1283
  path: input.path,
1264
1284
  bytesWritten: Buffer.byteLength(newContent2, "utf8"),
@@ -1307,8 +1327,8 @@ ${input.content}${contentNl}`;
1307
1327
  let newContent;
1308
1328
  switch (input.operation) {
1309
1329
  case "prepend": {
1310
- const sep3 = input.content.endsWith("\n") ? "" : "\n";
1311
- newContent = `${before}${input.content}${sep3}${section}${after}`;
1330
+ const sep4 = input.content.endsWith("\n") ? "" : "\n";
1331
+ newContent = `${before}${input.content}${sep4}${section}${after}`;
1312
1332
  break;
1313
1333
  }
1314
1334
  case "append": {
@@ -1353,7 +1373,7 @@ function updateCache(ctx2, path2, newContent, bodyStart) {
1353
1373
 
1354
1374
  // src/tools/periodic_note.ts
1355
1375
  import { access as access4, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
1356
- import { dirname as dirname3, join as join10 } from "path";
1376
+ import { dirname as dirname3, join as join3 } from "path";
1357
1377
  import { z as z14 } from "zod";
1358
1378
  var PERIOD = z14.enum(["daily", "weekly", "monthly", "quarterly", "yearly"]);
1359
1379
  var DEFAULTS = {
@@ -1373,7 +1393,7 @@ async function readJson(absPath) {
1373
1393
  async function resolveConfig(vaultRoot2, period) {
1374
1394
  const defaults = DEFAULTS[period];
1375
1395
  if (period === "daily") {
1376
- const cfg2 = await readJson(join10(vaultRoot2, ".obsidian", "daily-notes.json"));
1396
+ const cfg2 = await readJson(join3(vaultRoot2, ".obsidian", "daily-notes.json"));
1377
1397
  return {
1378
1398
  folder: cfg2?.folder ?? defaults.folder,
1379
1399
  format: cfg2?.format ?? defaults.format,
@@ -1381,7 +1401,7 @@ async function resolveConfig(vaultRoot2, period) {
1381
1401
  };
1382
1402
  }
1383
1403
  const cfg = await readJson(
1384
- join10(vaultRoot2, ".obsidian", "plugins", "periodic-notes", "data.json")
1404
+ join3(vaultRoot2, ".obsidian", "plugins", "periodic-notes", "data.json")
1385
1405
  );
1386
1406
  const entry = cfg?.[period];
1387
1407
  return {
@@ -1434,8 +1454,7 @@ async function getPeriodicNote(ctx2, rawInput) {
1434
1454
  const date = input.date ? new Date(input.date) : /* @__PURE__ */ new Date();
1435
1455
  const cfg = await resolveConfig(ctx2.vaultRoot, input.period);
1436
1456
  const path2 = resolveNotePath(cfg, date);
1437
- const abs = join10(ctx2.vaultRoot, path2);
1438
- if (!abs.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${path2}`);
1457
+ const abs = resolveVaultPath(ctx2.vaultRoot, path2);
1439
1458
  let existed = false;
1440
1459
  try {
1441
1460
  await access4(abs);
@@ -1447,15 +1466,13 @@ async function getPeriodicNote(ctx2, rawInput) {
1447
1466
  assertWritable(ctx2.policy, path2);
1448
1467
  let body = "";
1449
1468
  if (cfg.template) {
1450
- const templateAbs = join10(
1451
- ctx2.vaultRoot,
1452
- cfg.template.endsWith(".md") ? cfg.template : `${cfg.template}.md`
1453
- );
1454
- if (templateAbs.startsWith(ctx2.vaultRoot)) {
1455
- try {
1456
- body = await readFile6(templateAbs, "utf8");
1457
- } catch {
1458
- }
1469
+ try {
1470
+ const templateAbs = resolveVaultPath(
1471
+ ctx2.vaultRoot,
1472
+ cfg.template.endsWith(".md") ? cfg.template : `${cfg.template}.md`
1473
+ );
1474
+ body = await readFile6(templateAbs, "utf8");
1475
+ } catch {
1459
1476
  }
1460
1477
  }
1461
1478
  await mkdir4(dirname3(abs), { recursive: true });
@@ -1477,8 +1494,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
1477
1494
  const date = input.date ? new Date(input.date) : /* @__PURE__ */ new Date();
1478
1495
  const cfg = await resolveConfig(ctx2.vaultRoot, input.period);
1479
1496
  const path2 = resolveNotePath(cfg, date);
1480
- const abs = join10(ctx2.vaultRoot, path2);
1481
- if (!abs.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${path2}`);
1497
+ const abs = resolveVaultPath(ctx2.vaultRoot, path2);
1482
1498
  assertWritable(ctx2.policy, path2);
1483
1499
  let original = "";
1484
1500
  try {
@@ -1656,7 +1672,6 @@ function queryNotes(ctx2, input) {
1656
1672
 
1657
1673
  // src/tools/read_note.ts
1658
1674
  import { readFile as readFile7 } from "fs/promises";
1659
- import { join as join11 } from "path";
1660
1675
  import { z as z16 } from "zod";
1661
1676
  var ReadNoteInput = z16.object({
1662
1677
  path: z16.string().describe('Vault-relative path to the note, e.g. "Daily Notes/2026-05-29.md".'),
@@ -1673,10 +1688,7 @@ var ReadNoteInput = z16.object({
1673
1688
  message: "At most one of section, block, or lines may be specified."
1674
1689
  });
1675
1690
  async function readNote(ctx2, input) {
1676
- const absPath = join11(ctx2.vaultRoot, input.path);
1677
- if (!absPath.startsWith(ctx2.vaultRoot)) {
1678
- throw new Error(`Path outside vault: ${input.path}`);
1679
- }
1691
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1680
1692
  const raw = await readFile7(absPath, "utf8");
1681
1693
  const noteBytes = Buffer.byteLength(raw, "utf8");
1682
1694
  const hash = contentHash(raw);
@@ -1764,20 +1776,142 @@ async function readNote(ctx2, input) {
1764
1776
  };
1765
1777
  }
1766
1778
 
1767
- // src/tools/replace_in_note.ts
1779
+ // src/tools/rename_heading.ts
1768
1780
  import { readFile as readFile8 } from "fs/promises";
1769
- import { join as join12 } from "path";
1781
+ import { join as join4 } from "path";
1770
1782
  import { z as z17 } from "zod";
1771
- var ReplaceInNoteInput = z17.object({
1772
- path: z17.string().describe("Vault-relative path to the note."),
1773
- find: z17.string().min(1).max(1e3).describe("Text or pattern to find (max 1000 chars; bounds regex-mode pattern size)."),
1774
- replace: z17.string().describe("Replacement text. Supports $1, $2, \u2026 backreferences in regex mode."),
1775
- regex: z17.boolean().default(false).describe("Treat find as a regular expression."),
1776
- caseSensitive: z17.boolean().default(false).describe("Case-sensitive matching."),
1777
- wholeWord: z17.boolean().default(false).describe("Match whole words only (\\b word-boundary)."),
1778
- limit: z17.number().int().min(1).optional().describe("Maximum number of replacements. Omit to replace all."),
1779
- dryRun: z17.boolean().default(false).describe("If true, report matches without writing anything."),
1783
+ var RenameHeadingInput = z17.object({
1784
+ path: z17.string().describe("Vault-relative path of the note containing the heading."),
1785
+ oldHeading: z17.string().min(1).describe(
1786
+ "Current heading text, without # markers. Matched case-insensitively; if the note has duplicate headings the first one wins, mirroring Obsidian link resolution."
1787
+ ),
1788
+ newHeading: z17.string().min(1).refine((s) => !/[\n[\]|#]/.test(s), {
1789
+ message: 'newHeading must not contain newlines, "[", "]", "|", or "#" \u2014 those break [[note#heading]] link syntax.'
1790
+ }).describe("New heading text, without # markers."),
1780
1791
  prevHash: z17.string().optional().describe(
1792
+ "Optional compare-and-swap guard: the contentHash from a prior read of this note. Fails with hash_conflict if the note changed since."
1793
+ )
1794
+ });
1795
+ var HEADING_LINK_RE = /(!?)\[\[([^\]|#\n]{0,512})(#[^\]|\n]{1,512})(\|[^\]\n]{1,512})?\]\]/g;
1796
+ function rewriteHeadingLinks(raw, oldHeading, newHeading, matchesTarget) {
1797
+ const oldLower = oldHeading.toLowerCase();
1798
+ let count = 0;
1799
+ const content = mapUnfencedLines(
1800
+ raw,
1801
+ (line) => line.replace(
1802
+ HEADING_LINK_RE,
1803
+ (full, bang, target, frag, alias) => {
1804
+ const fragText = frag.slice(1);
1805
+ if (fragText.startsWith("^") || fragText.includes("#")) return full;
1806
+ if (fragText.trim().toLowerCase() !== oldLower) return full;
1807
+ if (!matchesTarget(target.trim())) return full;
1808
+ count++;
1809
+ return `${bang}[[${target}#${newHeading}${alias ?? ""}]]`;
1810
+ }
1811
+ )
1812
+ );
1813
+ return { content, count };
1814
+ }
1815
+ async function renameHeading(ctx2, rawInput) {
1816
+ const input = RenameHeadingInput.parse(rawInput);
1817
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1818
+ assertWritable(ctx2.policy, input.path);
1819
+ let raw;
1820
+ try {
1821
+ raw = await readFile8(absPath, "utf8");
1822
+ } catch {
1823
+ throw new Error(`Note not found: ${input.path}`);
1824
+ }
1825
+ if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
1826
+ const outline = buildOutline(raw, { includeBlocks: false });
1827
+ const heading = outline.headings.find(
1828
+ (h) => h.text.toLowerCase() === input.oldHeading.toLowerCase()
1829
+ );
1830
+ if (!heading) {
1831
+ throw new Error(
1832
+ JSON.stringify({
1833
+ error: "heading_not_found",
1834
+ target: input.oldHeading,
1835
+ available: outline.headings.map((h) => h.text)
1836
+ })
1837
+ );
1838
+ }
1839
+ const nl = raw.indexOf("\n", heading.byteOffset);
1840
+ const lineEnd2 = nl === -1 ? raw.length : nl;
1841
+ const lineText = raw.slice(heading.byteOffset, lineEnd2);
1842
+ const markerPrefix = /^#{1,6}\s+/.exec(lineText)?.[0] ?? "";
1843
+ const trailing = /\s*$/.exec(lineText)?.[0] ?? "";
1844
+ const newLineText = `${markerPrefix}${input.newHeading}${trailing}`;
1845
+ let newRaw = raw.slice(0, heading.byteOffset) + newLineText + raw.slice(lineEnd2);
1846
+ const fm = parseFrontmatter(newRaw);
1847
+ const bodyRegion = newRaw.slice(fm.bodyStart);
1848
+ const selfRewrite = rewriteHeadingLinks(
1849
+ bodyRegion,
1850
+ input.oldHeading,
1851
+ input.newHeading,
1852
+ (target) => target === "" || resolveLink(target, ctx2.notes) === input.path
1853
+ );
1854
+ newRaw = newRaw.slice(0, fm.bodyStart) + selfRewrite.content;
1855
+ const originalFmRegion = raw.slice(0, fm.bodyStart);
1856
+ await atomicWrite(absPath, newRaw);
1857
+ const written = await readFile8(absPath, "utf8");
1858
+ if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
1859
+ throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
1860
+ }
1861
+ removeNoteBacklinks(ctx2, input.path);
1862
+ upsertDoc(ctx2, buildDoc(input.path, newRaw));
1863
+ addNoteBacklinks(ctx2, input.path, newRaw);
1864
+ let notesRewritten = selfRewrite.count > 0 ? 1 : 0;
1865
+ let linksRewritten = selfRewrite.count;
1866
+ const skipped = [];
1867
+ const candidates = /* @__PURE__ */ new Set();
1868
+ for (const ref of ctx2.backlinks.get(input.path) ?? []) candidates.add(ref.path);
1869
+ candidates.delete(input.path);
1870
+ for (const path2 of [...candidates].sort()) {
1871
+ const doc = ctx2.notes.get(path2);
1872
+ if (doc === void 0) continue;
1873
+ const { content, count } = rewriteHeadingLinks(
1874
+ doc.raw,
1875
+ input.oldHeading,
1876
+ input.newHeading,
1877
+ (target) => target !== "" && resolveLink(target, ctx2.notes) === input.path
1878
+ );
1879
+ if (count === 0) continue;
1880
+ if (!isWritable(ctx2.policy, path2)) {
1881
+ skipped.push(path2);
1882
+ continue;
1883
+ }
1884
+ await atomicWrite(join4(ctx2.vaultRoot, path2), content);
1885
+ upsertDoc(ctx2, buildDoc(path2, content));
1886
+ notesRewritten++;
1887
+ linksRewritten += count;
1888
+ }
1889
+ const result = {
1890
+ path: input.path,
1891
+ oldHeading: input.oldHeading,
1892
+ newHeading: input.newHeading,
1893
+ line: heading.line,
1894
+ notesRewritten,
1895
+ linksRewritten,
1896
+ contentHash: contentHash(newRaw)
1897
+ };
1898
+ if (skipped.length > 0) result.skipped = skipped;
1899
+ return result;
1900
+ }
1901
+
1902
+ // src/tools/replace_in_note.ts
1903
+ import { readFile as readFile9 } from "fs/promises";
1904
+ import { z as z18 } from "zod";
1905
+ var ReplaceInNoteInput = z18.object({
1906
+ path: z18.string().describe("Vault-relative path to the note."),
1907
+ find: z18.string().min(1).max(1e3).describe("Text or pattern to find (max 1000 chars; bounds regex-mode pattern size)."),
1908
+ replace: z18.string().describe("Replacement text. Supports $1, $2, \u2026 backreferences in regex mode."),
1909
+ regex: z18.boolean().default(false).describe("Treat find as a regular expression."),
1910
+ caseSensitive: z18.boolean().default(false).describe("Case-sensitive matching."),
1911
+ wholeWord: z18.boolean().default(false).describe("Match whole words only (\\b word-boundary)."),
1912
+ limit: z18.number().int().min(1).optional().describe("Maximum number of replacements. Omit to replace all."),
1913
+ dryRun: z18.boolean().default(false).describe("If true, report matches without writing anything."),
1914
+ prevHash: z18.string().optional().describe(
1781
1915
  "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
1782
1916
  )
1783
1917
  });
@@ -1791,10 +1925,7 @@ function buildRegex(find, opts) {
1791
1925
  return new RegExp(pattern, flags);
1792
1926
  }
1793
1927
  async function replaceInNote(ctx2, input) {
1794
- const absPath = join12(ctx2.vaultRoot, input.path);
1795
- if (!absPath.startsWith(ctx2.vaultRoot)) {
1796
- throw new Error(`Path outside vault: ${input.path}`);
1797
- }
1928
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1798
1929
  assertWritable(ctx2.policy, input.path);
1799
1930
  let re;
1800
1931
  try {
@@ -1806,7 +1937,7 @@ async function replaceInNote(ctx2, input) {
1806
1937
  } catch (err) {
1807
1938
  throw new Error(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`);
1808
1939
  }
1809
- const raw = await readFile8(absPath, "utf8");
1940
+ const raw = await readFile9(absPath, "utf8");
1810
1941
  if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
1811
1942
  const fm = parseFrontmatter(raw);
1812
1943
  const originalFmRegion = raw.slice(0, fm.bodyStart);
@@ -1838,7 +1969,7 @@ async function replaceInNote(ctx2, input) {
1838
1969
  newBody += body.slice(lastIndex);
1839
1970
  const newContent = `${originalFmRegion}${newBody}`;
1840
1971
  await atomicWrite(absPath, newContent);
1841
- const written = await readFile8(absPath, "utf8");
1972
+ const written = await readFile9(absPath, "utf8");
1842
1973
  if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
1843
1974
  throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
1844
1975
  }
@@ -1863,6 +1994,7 @@ var WRITE_TOOLS = /* @__PURE__ */ new Set([
1863
1994
  "create_note",
1864
1995
  "delete_note",
1865
1996
  "move_note",
1997
+ "rename_heading",
1866
1998
  "append_note",
1867
1999
  "patch_frontmatter",
1868
2000
  "patch_note",
@@ -2006,6 +2138,15 @@ async function run(ctx2, name, args) {
2006
2138
  }
2007
2139
  return { content: [{ type: "text", text }] };
2008
2140
  }
2141
+ case "rename_heading": {
2142
+ const input = RenameHeadingInput.parse(args);
2143
+ const result = await renameHeading(ctx2, input);
2144
+ let text = `Renamed heading "${result.oldHeading}" \u2192 "${result.newHeading}" in ${result.path} (line ${result.line}). Rewrote ${result.linksRewritten} link${result.linksRewritten === 1 ? "" : "s"} in ${result.notesRewritten} note${result.notesRewritten === 1 ? "" : "s"}. contentHash: ${result.contentHash}`;
2145
+ if (result.skipped?.length) {
2146
+ text += ` Skipped (outside SEEKSTONE_WRITE_PATHS): ${result.skipped.join(", ")}.`;
2147
+ }
2148
+ return { content: [{ type: "text", text }] };
2149
+ }
2009
2150
  case "append_note": {
2010
2151
  const input = AppendNoteInput.parse(args);
2011
2152
  const result = await appendNote(ctx2, input);
@@ -2071,12 +2212,12 @@ async function run(ctx2, name, args) {
2071
2212
  }
2072
2213
 
2073
2214
  // src/index/build.ts
2074
- import { readFile as readFile9 } from "fs/promises";
2215
+ import { readFile as readFile10 } from "fs/promises";
2075
2216
  import { basename as basename3, extname as extname4 } from "path";
2076
2217
 
2077
2218
  // ../core/src/walk.ts
2078
2219
  import { stat } from "fs/promises";
2079
- import { extname as extname3, join as join13, relative, sep } from "path";
2220
+ import { extname as extname3, join as join5, relative, sep as sep2 } from "path";
2080
2221
  import fg from "fast-glob";
2081
2222
  var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".heic"]);
2082
2223
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
@@ -2118,7 +2259,7 @@ async function walkVault(vaultRoot2) {
2118
2259
  const stats = typeof m === "string" ? await stat(absPath) : m.stats;
2119
2260
  if (!stats) continue;
2120
2261
  const relPath = relative(vaultRoot2, absPath);
2121
- const topDir = relPath.split(sep)[0] ?? "";
2262
+ const topDir = relPath.split(sep2)[0] ?? "";
2122
2263
  entries.push({
2123
2264
  absPath,
2124
2265
  relPath,
@@ -2153,7 +2294,7 @@ async function buildIndex(vaultRoot2) {
2153
2294
  await Promise.all(
2154
2295
  noteEntries.map(async (entry) => {
2155
2296
  try {
2156
- const raw = await readFile9(entry.absPath, "utf8");
2297
+ const raw = await readFile10(entry.absPath, "utf8");
2157
2298
  const fm = parseFrontmatter(raw);
2158
2299
  const inlineTags = extractInlineTags(fm.body);
2159
2300
  const fmTags = frontmatterTags(fm.data);
@@ -2206,8 +2347,8 @@ function buildBacklinks(notes2) {
2206
2347
 
2207
2348
  // src/init.ts
2208
2349
  import { execFileSync } from "child_process";
2209
- import { access as access5, copyFile, mkdir as mkdir5, readdir, readFile as readFile10, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2210
- import path, { dirname as dirname4, join as join14 } from "path";
2350
+ import { access as access5, copyFile, mkdir as mkdir5, readdir, readFile as readFile11, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2351
+ import path, { dirname as dirname4, join as join6 } from "path";
2211
2352
  function parseInitArgs(argv) {
2212
2353
  const opts = { write: false, client: "desktop" };
2213
2354
  for (let i = 0; i < argv.length; i++) {
@@ -2250,7 +2391,7 @@ function cursorConfigPath(platform, env) {
2250
2391
  return j(env.home ?? "", ".cursor", "mcp.json");
2251
2392
  }
2252
2393
  function vscodeMcpConfigPath(cwd) {
2253
- return join14(cwd, ".vscode", "mcp.json");
2394
+ return join6(cwd, ".vscode", "mcp.json");
2254
2395
  }
2255
2396
  function obsidianRegistryPath(platform, env) {
2256
2397
  if (platform === "win32") {
@@ -2312,7 +2453,7 @@ async function validateVault(vaultPath) {
2312
2453
  return { ok: false, error: `Path does not exist: ${vaultPath}` };
2313
2454
  }
2314
2455
  try {
2315
- await access5(join14(vaultPath, ".obsidian"));
2456
+ await access5(join6(vaultPath, ".obsidian"));
2316
2457
  } catch {
2317
2458
  return {
2318
2459
  ok: false,
@@ -2332,7 +2473,7 @@ async function countMarkdown(root) {
2332
2473
  }
2333
2474
  for (const e of entries) {
2334
2475
  if (e.name.startsWith(".")) continue;
2335
- const full = join14(dir, e.name);
2476
+ const full = join6(dir, e.name);
2336
2477
  if (e.isDirectory()) await walk(full);
2337
2478
  else if (e.name.endsWith(".md")) count++;
2338
2479
  }
@@ -2350,7 +2491,7 @@ async function runInit(opts, deps) {
2350
2491
  ]
2351
2492
  };
2352
2493
  }
2353
- const rf = deps.readFile ?? readFile10;
2494
+ const rf = deps.readFile ?? readFile11;
2354
2495
  let vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT;
2355
2496
  if (!vaultPath) {
2356
2497
  const registryPath = obsidianRegistryPath(deps.platform, {
@@ -2835,6 +2976,32 @@ var ALL_TOOLS = [
2835
2976
  required: ["from", "to"]
2836
2977
  }
2837
2978
  },
2979
+ {
2980
+ name: "rename_heading",
2981
+ 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
+ inputSchema: {
2983
+ type: "object",
2984
+ properties: {
2985
+ path: {
2986
+ type: "string",
2987
+ description: "Vault-relative path of the note containing the heading."
2988
+ },
2989
+ oldHeading: {
2990
+ type: "string",
2991
+ description: "Current heading text, without # markers. Case-insensitive; first match wins."
2992
+ },
2993
+ newHeading: {
2994
+ type: "string",
2995
+ description: 'New heading text, without # markers. Must not contain newlines, "[", "]", "|", or "#".'
2996
+ },
2997
+ prevHash: {
2998
+ type: "string",
2999
+ description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
3000
+ }
3001
+ },
3002
+ required: ["path", "oldHeading", "newHeading"]
3003
+ }
3004
+ },
2838
3005
  {
2839
3006
  name: "append_note",
2840
3007
  description: "Append text to a note body without touching the frontmatter. Safe for meeting notes, daily logs, and append-only workflows.",
@@ -3051,8 +3218,8 @@ function visibleTools(policy2) {
3051
3218
 
3052
3219
  // src/watcher.ts
3053
3220
  import { realpathSync } from "fs";
3054
- import { readFile as readFile11 } from "fs/promises";
3055
- import { join as join15, relative as relative2, sep as sep2 } from "path";
3221
+ import { readFile as readFile12 } from "fs/promises";
3222
+ import { join as join7, relative as relative2, sep as sep3 } from "path";
3056
3223
  import chokidar from "chokidar";
3057
3224
  function startWatcher(ctx2, log2, opts) {
3058
3225
  const usePolling = opts?.usePolling ?? process.env.SEEKSTONE_WATCH_POLL === "1";
@@ -3061,10 +3228,10 @@ function startWatcher(ctx2, log2, opts) {
3061
3228
  vaultRoot2 = realpathSync(ctx2.vaultRoot);
3062
3229
  } catch {
3063
3230
  }
3064
- const toRel = (abs) => relative2(vaultRoot2, abs).split(sep2).join("/");
3231
+ const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
3065
3232
  async function upsert(relPath, op) {
3066
3233
  try {
3067
- const raw = await readFile11(join15(vaultRoot2, relPath), "utf8");
3234
+ const raw = await readFile12(join7(vaultRoot2, relPath), "utf8");
3068
3235
  removeNoteBacklinks(ctx2, relPath);
3069
3236
  upsertDoc(ctx2, buildDoc(relPath, raw));
3070
3237
  addNoteBacklinks(ctx2, relPath, raw);
@@ -3096,7 +3263,7 @@ function startWatcher(ctx2, log2, opts) {
3096
3263
  ignored: (p) => {
3097
3264
  const rel = relative2(vaultRoot2, p);
3098
3265
  if (!rel || rel === ".") return false;
3099
- return rel.split(sep2).filter(Boolean).some((seg) => seg.startsWith(".") && seg !== "..");
3266
+ return rel.split(sep3).filter(Boolean).some((seg) => seg.startsWith(".") && seg !== "..");
3100
3267
  }
3101
3268
  });
3102
3269
  watcher2.on("add", onUpsert("add")).on("change", onUpsert("change")).on("unlink", (abs) => {
@@ -3104,9 +3271,9 @@ function startWatcher(ctx2, log2, opts) {
3104
3271
  opts?.onRawEvent?.("unlink", abs, relPath);
3105
3272
  if (relPath.endsWith(".md")) remove(relPath);
3106
3273
  });
3107
- const ready = new Promise((resolve) => {
3274
+ const ready = new Promise((resolve3) => {
3108
3275
  watcher2.once("ready", () => {
3109
- resolve();
3276
+ resolve3();
3110
3277
  });
3111
3278
  });
3112
3279
  return {
@@ -3116,7 +3283,7 @@ function startWatcher(ctx2, log2, opts) {
3116
3283
  }
3117
3284
 
3118
3285
  // src/index.ts
3119
- var VERSION = true ? "0.12.1" : "0.0.0-dev";
3286
+ var VERSION = true ? "0.13.0" : "0.0.0-dev";
3120
3287
  var intent = parseCliIntent(process.argv.slice(2));
3121
3288
  if (intent === "version") {
3122
3289
  process.stdout.write(`${VERSION}
@@ -3146,11 +3313,12 @@ if (intent === "init") {
3146
3313
  }
3147
3314
  var log = createLogger();
3148
3315
  installProcessGuards(log);
3149
- var vaultRoot = process.env.SEEKSTONE_VAULT;
3150
- if (!vaultRoot) {
3316
+ var rawVaultRoot = process.env.SEEKSTONE_VAULT;
3317
+ if (!rawVaultRoot) {
3151
3318
  log.error("SEEKSTONE_VAULT env var is required");
3152
3319
  process.exit(1);
3153
3320
  }
3321
+ var vaultRoot = resolve2(rawVaultRoot);
3154
3322
  var policy = parseWritePolicy(process.env);
3155
3323
  if (policy.readOnly) log.info("read-only mode", { env: "SEEKSTONE_READ_ONLY" });
3156
3324
  if (policy.writeGlobs) log.info("write paths scoped", { globs: policy.writeGlobs });