seekstone 0.12.1 → 0.12.2

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
@@ -110,7 +110,7 @@ Other MCP clients (Windsurf, Cline, …) take the Option 3 JSON block in their o
110
110
 
111
111
  | Tool | Description |
112
112
  |---|---|
113
- | `search` | Full-text search. Returns ranked excerpts (default ~120 chars, tunable via `excerptLength`), not full notes. Fuzzy, prefix, and phrase queries. |
113
+ | `search` | Full-text search. Returns ranked excerpts (default ~120 chars, tunable via `excerptLength`), not full notes. Fuzzy and prefix matching. |
114
114
  | `query_notes` | Structured metadata query. Filter by frontmatter key/value predicates (`eq`, `ne`, `contains`, `exists`, `missing`, `gt`/`gte`/`lt`/`lte`), tag, folder, modified time, and size; sort and select the fields you need. Returns compact rows, not note content. |
115
115
  | `context_pack` | Answer-ready context for a natural-language question in one call, hard-capped at a byte budget (default 2 KB): ranked excerpts, linked neighbor notes with one-line summaries, and follow-up source paths — replaces a search → read → get_backlinks round-trip loop. |
116
116
  | `read_note` | Read the full content of a note by vault-relative path. Supports returning a single section, block, or line range. |
@@ -131,10 +131,10 @@ Other MCP clients (Windsurf, Cline, …) take the Option 3 JSON block in their o
131
131
  | `append_note` | Append to a note body without touching frontmatter. |
132
132
  | `patch_frontmatter` | Set/update/delete frontmatter keys without reordering existing keys or changing quote style. |
133
133
  | `patch_note` | Insert text immediately after a heading without touching frontmatter. |
134
- | `replace_in_note` | Replace the first occurrence of a word or phrase in the note body. |
134
+ | `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. |
135
135
  | `append_periodic_note` | Append to today's periodic note, creating it from a template if it doesn't yet exist. |
136
136
 
137
- Every edit tool supports 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.
137
+ 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
138
 
139
139
  ---
140
140
 
@@ -146,6 +146,7 @@ Every edit tool supports optional **compare-and-swap**: pass the `contentHash` y
146
146
  | `SEEKSTONE_LOG_LEVEL` | no | `error` \| `warn` \| `info` (default) \| `debug`. |
147
147
  | `SEEKSTONE_LOG_FILE` | no | Absolute path; when set, JSON-line logs are appended here (size-rotated). |
148
148
  | `SEEKSTONE_WATCH_POLL` | no | Set to `1` to stat-poll for changes instead of native OS events — reliable on network drives, WSL, containers. |
149
+ | `SEEKSTONE_LOG_MAX_SIZE` | no | Log-rotation threshold for `SEEKSTONE_LOG_FILE` (e.g. `10mb`; default 5 MB). |
149
150
  | `SEEKSTONE_READ_ONLY` | no | Set to `1` to run read-only: the 8 write tools are unregistered entirely (and rejected if called anyway), so the session provably cannot modify your vault. |
150
151
  | `SEEKSTONE_WRITE_PATHS` | no | Comma-separated vault-relative globs (e.g. `journal/**,inbox/*.md`). Writes are permitted only under matching paths; the rest of the vault stays read-only. |
151
152
 
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);
@@ -565,7 +572,7 @@ function contextPack(ctx2, input) {
565
572
 
566
573
  // src/tools/create_note.ts
567
574
  import { access, mkdir, readFile as readFile2 } from "fs/promises";
568
- import { dirname, join as join2 } from "path";
575
+ import { dirname } from "path";
569
576
  import { stringify as stringifyYaml } from "yaml";
570
577
  import { z as z4 } from "zod";
571
578
 
@@ -611,10 +618,7 @@ var CreateNoteInput = z4.object({
611
618
  });
612
619
  async function createNote(ctx2, rawInput) {
613
620
  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
- }
621
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
618
622
  assertWritable(ctx2.policy, input.path);
619
623
  if (!input.overwrite) {
620
624
  try {
@@ -659,7 +663,7 @@ ${stringifyYaml(input.frontmatter, { lineWidth: 0 })}---
659
663
 
660
664
  // src/tools/delete_note.ts
661
665
  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";
666
+ import { basename as basename2, extname as extname2, join } from "path";
663
667
  import { z as z5 } from "zod";
664
668
 
665
669
  // src/index/backlinks.ts
@@ -707,7 +711,7 @@ async function freeTrashPath(vaultRoot2, noteBase) {
707
711
  let candidate = `.trash/${noteBase}`;
708
712
  for (; ; ) {
709
713
  try {
710
- await access2(join3(vaultRoot2, candidate));
714
+ await access2(join(vaultRoot2, candidate));
711
715
  } catch {
712
716
  return candidate;
713
717
  }
@@ -716,19 +720,16 @@ async function freeTrashPath(vaultRoot2, noteBase) {
716
720
  }
717
721
  async function deleteNote(ctx2, rawInput) {
718
722
  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
- }
723
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
723
724
  assertWritable(ctx2.policy, input.path);
724
725
  let trashedTo;
725
726
  if (input.permanent) {
726
727
  await rm(absPath);
727
728
  } else {
728
729
  await access2(absPath);
729
- await mkdir2(join3(ctx2.vaultRoot, ".trash"), { recursive: true });
730
+ await mkdir2(join(ctx2.vaultRoot, ".trash"), { recursive: true });
730
731
  trashedTo = await freeTrashPath(ctx2.vaultRoot, basename2(input.path));
731
- await rename2(absPath, join3(ctx2.vaultRoot, trashedTo));
732
+ await rename2(absPath, join(ctx2.vaultRoot, trashedTo));
732
733
  }
733
734
  removeNoteBacklinks(ctx2, input.path);
734
735
  if (ctx2.notes.has(input.path)) {
@@ -741,7 +742,6 @@ async function deleteNote(ctx2, rawInput) {
741
742
  }
742
743
 
743
744
  // src/tools/get_backlinks.ts
744
- import { join as join4 } from "path";
745
745
  import { z as z6 } from "zod";
746
746
  var GetBacklinksInput = z6.object({
747
747
  path: z6.string().describe("Vault-relative path to the target note."),
@@ -750,10 +750,7 @@ var GetBacklinksInput = z6.object({
750
750
  });
751
751
  var EXCERPT_MAX = 200;
752
752
  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
- }
753
+ resolveVaultPath(ctx2.vaultRoot, input.path);
757
754
  const includeContext = input.includeContext ?? true;
758
755
  const limit = input.limit ?? 50;
759
756
  const refs = ctx2.backlinks.get(input.path) ?? [];
@@ -778,16 +775,12 @@ function getBacklinks(ctx2, input) {
778
775
  }
779
776
 
780
777
  // src/tools/get_links.ts
781
- import { join as join5 } from "path";
782
778
  import { z as z7 } from "zod";
783
779
  var GetLinksInput = z7.object({
784
780
  path: z7.string().describe("Vault-relative path to the note.")
785
781
  });
786
782
  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
- }
783
+ resolveVaultPath(ctx2.vaultRoot, input.path);
791
784
  const note = ctx2.notes.get(input.path);
792
785
  if (note === void 0) throw new Error(`Note not found: ${input.path}`);
793
786
  const rawLinks = extractLinksWithLines(note.raw);
@@ -872,7 +865,7 @@ function listTags(ctx2, input) {
872
865
 
873
866
  // src/tools/move_note.ts
874
867
  import { access as access3, mkdir as mkdir3, rename as rename3 } from "fs/promises";
875
- import { dirname as dirname2, join as join6 } from "path";
868
+ import { dirname as dirname2, join as join2 } from "path";
876
869
  import { z as z10 } from "zod";
877
870
 
878
871
  // src/tools/rewrite_links.ts
@@ -976,10 +969,8 @@ var MoveNoteInput = z10.object({
976
969
  });
977
970
  async function moveNote(ctx2, rawInput) {
978
971
  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}`);
972
+ const absFrom = resolveVaultPath(ctx2.vaultRoot, input.from);
973
+ const absTo = resolveVaultPath(ctx2.vaultRoot, input.to);
983
974
  assertWritable(ctx2.policy, input.from);
984
975
  assertWritable(ctx2.policy, input.to);
985
976
  try {
@@ -1037,7 +1028,7 @@ async function moveNote(ctx2, rawInput) {
1037
1028
  );
1038
1029
  removeNoteBacklinks(ctx2, path2);
1039
1030
  if (count > 0) {
1040
- await atomicWrite(join6(ctx2.vaultRoot, path2), content);
1031
+ await atomicWrite(join2(ctx2.vaultRoot, path2), content);
1041
1032
  upsertDoc(ctx2, buildDoc(path2, content));
1042
1033
  notesRewritten++;
1043
1034
  linksRewritten += count;
@@ -1054,7 +1045,6 @@ async function moveNote(ctx2, rawInput) {
1054
1045
 
1055
1046
  // src/tools/outline_note.ts
1056
1047
  import { readFile as readFile3 } from "fs/promises";
1057
- import { join as join7 } from "path";
1058
1048
 
1059
1049
  // ../core/src/outline.ts
1060
1050
  var HEADING_RE = /^(#{1,6})\s+(.+)/;
@@ -1111,10 +1101,7 @@ var OutlineNoteInput = z11.object({
1111
1101
  includeSizes: z11.boolean().default(false).describe("Include per-section character length in each heading entry. Default false.")
1112
1102
  });
1113
1103
  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
- }
1104
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1118
1105
  const raw = await readFile3(absPath, "utf8");
1119
1106
  return buildOutline(raw, {
1120
1107
  includeBlocks: input.includeBlocks,
@@ -1124,7 +1111,6 @@ async function outlineNote(ctx2, input) {
1124
1111
 
1125
1112
  // src/tools/patch_frontmatter.ts
1126
1113
  import { readFile as readFile4 } from "fs/promises";
1127
- import { join as join8 } from "path";
1128
1114
  import { parseDocument } from "yaml";
1129
1115
  import { z as z12 } from "zod";
1130
1116
  var PatchFrontmatterInput = z12.object({
@@ -1137,10 +1123,7 @@ var PatchFrontmatterInput = z12.object({
1137
1123
  )
1138
1124
  });
1139
1125
  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
- }
1126
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1144
1127
  assertWritable(ctx2.policy, input.path);
1145
1128
  const original = await readFile4(absPath, "utf8");
1146
1129
  if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
@@ -1203,7 +1186,6 @@ ${original}`;
1203
1186
 
1204
1187
  // src/tools/patch_note.ts
1205
1188
  import { readFile as readFile5 } from "fs/promises";
1206
- import { join as join9 } from "path";
1207
1189
  import { z as z13 } from "zod";
1208
1190
  var PatchNoteInput = z13.object({
1209
1191
  path: z13.string().describe("Vault-relative path to the note."),
@@ -1232,10 +1214,7 @@ function lineStart(raw, offset) {
1232
1214
  return prev === -1 ? 0 : prev + 1;
1233
1215
  }
1234
1216
  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
- }
1217
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1239
1218
  assertWritable(ctx2.policy, input.path);
1240
1219
  const raw = await readFile5(absPath, "utf8");
1241
1220
  if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
@@ -1250,15 +1229,15 @@ async function patchNote(ctx2, input) {
1250
1229
  const heading = outline.headings.find((h) => h.text.toLowerCase() === targetText.toLowerCase());
1251
1230
  if (!heading) {
1252
1231
  if (input.createIfMissing) {
1253
- const sep3 = raw.endsWith("\n\n") ? "" : raw.endsWith("\n") ? "\n" : "\n\n";
1232
+ const sep4 = raw.endsWith("\n\n") ? "" : raw.endsWith("\n") ? "\n" : "\n\n";
1254
1233
  const contentNl = input.content.endsWith("\n") ? "" : "\n";
1255
- const newContent2 = `${raw}${sep3}## ${targetText}
1234
+ const newContent2 = `${raw}${sep4}## ${targetText}
1256
1235
 
1257
1236
  ${input.content}${contentNl}`;
1258
1237
  await atomicWrite(absPath, newContent2);
1259
1238
  await verifyFrontmatter(absPath, originalFmRegion);
1260
1239
  updateCache(ctx2, input.path, newContent2, fm.bodyStart);
1261
- const headingOffset = raw.length + sep3.length;
1240
+ const headingOffset = raw.length + sep4.length;
1262
1241
  return {
1263
1242
  path: input.path,
1264
1243
  bytesWritten: Buffer.byteLength(newContent2, "utf8"),
@@ -1307,8 +1286,8 @@ ${input.content}${contentNl}`;
1307
1286
  let newContent;
1308
1287
  switch (input.operation) {
1309
1288
  case "prepend": {
1310
- const sep3 = input.content.endsWith("\n") ? "" : "\n";
1311
- newContent = `${before}${input.content}${sep3}${section}${after}`;
1289
+ const sep4 = input.content.endsWith("\n") ? "" : "\n";
1290
+ newContent = `${before}${input.content}${sep4}${section}${after}`;
1312
1291
  break;
1313
1292
  }
1314
1293
  case "append": {
@@ -1353,7 +1332,7 @@ function updateCache(ctx2, path2, newContent, bodyStart) {
1353
1332
 
1354
1333
  // src/tools/periodic_note.ts
1355
1334
  import { access as access4, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
1356
- import { dirname as dirname3, join as join10 } from "path";
1335
+ import { dirname as dirname3, join as join3 } from "path";
1357
1336
  import { z as z14 } from "zod";
1358
1337
  var PERIOD = z14.enum(["daily", "weekly", "monthly", "quarterly", "yearly"]);
1359
1338
  var DEFAULTS = {
@@ -1373,7 +1352,7 @@ async function readJson(absPath) {
1373
1352
  async function resolveConfig(vaultRoot2, period) {
1374
1353
  const defaults = DEFAULTS[period];
1375
1354
  if (period === "daily") {
1376
- const cfg2 = await readJson(join10(vaultRoot2, ".obsidian", "daily-notes.json"));
1355
+ const cfg2 = await readJson(join3(vaultRoot2, ".obsidian", "daily-notes.json"));
1377
1356
  return {
1378
1357
  folder: cfg2?.folder ?? defaults.folder,
1379
1358
  format: cfg2?.format ?? defaults.format,
@@ -1381,7 +1360,7 @@ async function resolveConfig(vaultRoot2, period) {
1381
1360
  };
1382
1361
  }
1383
1362
  const cfg = await readJson(
1384
- join10(vaultRoot2, ".obsidian", "plugins", "periodic-notes", "data.json")
1363
+ join3(vaultRoot2, ".obsidian", "plugins", "periodic-notes", "data.json")
1385
1364
  );
1386
1365
  const entry = cfg?.[period];
1387
1366
  return {
@@ -1434,8 +1413,7 @@ async function getPeriodicNote(ctx2, rawInput) {
1434
1413
  const date = input.date ? new Date(input.date) : /* @__PURE__ */ new Date();
1435
1414
  const cfg = await resolveConfig(ctx2.vaultRoot, input.period);
1436
1415
  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}`);
1416
+ const abs = resolveVaultPath(ctx2.vaultRoot, path2);
1439
1417
  let existed = false;
1440
1418
  try {
1441
1419
  await access4(abs);
@@ -1447,15 +1425,13 @@ async function getPeriodicNote(ctx2, rawInput) {
1447
1425
  assertWritable(ctx2.policy, path2);
1448
1426
  let body = "";
1449
1427
  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
- }
1428
+ try {
1429
+ const templateAbs = resolveVaultPath(
1430
+ ctx2.vaultRoot,
1431
+ cfg.template.endsWith(".md") ? cfg.template : `${cfg.template}.md`
1432
+ );
1433
+ body = await readFile6(templateAbs, "utf8");
1434
+ } catch {
1459
1435
  }
1460
1436
  }
1461
1437
  await mkdir4(dirname3(abs), { recursive: true });
@@ -1477,8 +1453,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
1477
1453
  const date = input.date ? new Date(input.date) : /* @__PURE__ */ new Date();
1478
1454
  const cfg = await resolveConfig(ctx2.vaultRoot, input.period);
1479
1455
  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}`);
1456
+ const abs = resolveVaultPath(ctx2.vaultRoot, path2);
1482
1457
  assertWritable(ctx2.policy, path2);
1483
1458
  let original = "";
1484
1459
  try {
@@ -1656,7 +1631,6 @@ function queryNotes(ctx2, input) {
1656
1631
 
1657
1632
  // src/tools/read_note.ts
1658
1633
  import { readFile as readFile7 } from "fs/promises";
1659
- import { join as join11 } from "path";
1660
1634
  import { z as z16 } from "zod";
1661
1635
  var ReadNoteInput = z16.object({
1662
1636
  path: z16.string().describe('Vault-relative path to the note, e.g. "Daily Notes/2026-05-29.md".'),
@@ -1673,10 +1647,7 @@ var ReadNoteInput = z16.object({
1673
1647
  message: "At most one of section, block, or lines may be specified."
1674
1648
  });
1675
1649
  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
- }
1650
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1680
1651
  const raw = await readFile7(absPath, "utf8");
1681
1652
  const noteBytes = Buffer.byteLength(raw, "utf8");
1682
1653
  const hash = contentHash(raw);
@@ -1766,7 +1737,6 @@ async function readNote(ctx2, input) {
1766
1737
 
1767
1738
  // src/tools/replace_in_note.ts
1768
1739
  import { readFile as readFile8 } from "fs/promises";
1769
- import { join as join12 } from "path";
1770
1740
  import { z as z17 } from "zod";
1771
1741
  var ReplaceInNoteInput = z17.object({
1772
1742
  path: z17.string().describe("Vault-relative path to the note."),
@@ -1791,10 +1761,7 @@ function buildRegex(find, opts) {
1791
1761
  return new RegExp(pattern, flags);
1792
1762
  }
1793
1763
  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
- }
1764
+ const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
1798
1765
  assertWritable(ctx2.policy, input.path);
1799
1766
  let re;
1800
1767
  try {
@@ -2076,7 +2043,7 @@ import { basename as basename3, extname as extname4 } from "path";
2076
2043
 
2077
2044
  // ../core/src/walk.ts
2078
2045
  import { stat } from "fs/promises";
2079
- import { extname as extname3, join as join13, relative, sep } from "path";
2046
+ import { extname as extname3, join as join4, relative, sep as sep2 } from "path";
2080
2047
  import fg from "fast-glob";
2081
2048
  var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".heic"]);
2082
2049
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
@@ -2118,7 +2085,7 @@ async function walkVault(vaultRoot2) {
2118
2085
  const stats = typeof m === "string" ? await stat(absPath) : m.stats;
2119
2086
  if (!stats) continue;
2120
2087
  const relPath = relative(vaultRoot2, absPath);
2121
- const topDir = relPath.split(sep)[0] ?? "";
2088
+ const topDir = relPath.split(sep2)[0] ?? "";
2122
2089
  entries.push({
2123
2090
  absPath,
2124
2091
  relPath,
@@ -2207,7 +2174,7 @@ function buildBacklinks(notes2) {
2207
2174
  // src/init.ts
2208
2175
  import { execFileSync } from "child_process";
2209
2176
  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";
2177
+ import path, { dirname as dirname4, join as join5 } from "path";
2211
2178
  function parseInitArgs(argv) {
2212
2179
  const opts = { write: false, client: "desktop" };
2213
2180
  for (let i = 0; i < argv.length; i++) {
@@ -2250,7 +2217,7 @@ function cursorConfigPath(platform, env) {
2250
2217
  return j(env.home ?? "", ".cursor", "mcp.json");
2251
2218
  }
2252
2219
  function vscodeMcpConfigPath(cwd) {
2253
- return join14(cwd, ".vscode", "mcp.json");
2220
+ return join5(cwd, ".vscode", "mcp.json");
2254
2221
  }
2255
2222
  function obsidianRegistryPath(platform, env) {
2256
2223
  if (platform === "win32") {
@@ -2312,7 +2279,7 @@ async function validateVault(vaultPath) {
2312
2279
  return { ok: false, error: `Path does not exist: ${vaultPath}` };
2313
2280
  }
2314
2281
  try {
2315
- await access5(join14(vaultPath, ".obsidian"));
2282
+ await access5(join5(vaultPath, ".obsidian"));
2316
2283
  } catch {
2317
2284
  return {
2318
2285
  ok: false,
@@ -2332,7 +2299,7 @@ async function countMarkdown(root) {
2332
2299
  }
2333
2300
  for (const e of entries) {
2334
2301
  if (e.name.startsWith(".")) continue;
2335
- const full = join14(dir, e.name);
2302
+ const full = join5(dir, e.name);
2336
2303
  if (e.isDirectory()) await walk(full);
2337
2304
  else if (e.name.endsWith(".md")) count++;
2338
2305
  }
@@ -3052,7 +3019,7 @@ function visibleTools(policy2) {
3052
3019
  // src/watcher.ts
3053
3020
  import { realpathSync } from "fs";
3054
3021
  import { readFile as readFile11 } from "fs/promises";
3055
- import { join as join15, relative as relative2, sep as sep2 } from "path";
3022
+ import { join as join6, relative as relative2, sep as sep3 } from "path";
3056
3023
  import chokidar from "chokidar";
3057
3024
  function startWatcher(ctx2, log2, opts) {
3058
3025
  const usePolling = opts?.usePolling ?? process.env.SEEKSTONE_WATCH_POLL === "1";
@@ -3061,10 +3028,10 @@ function startWatcher(ctx2, log2, opts) {
3061
3028
  vaultRoot2 = realpathSync(ctx2.vaultRoot);
3062
3029
  } catch {
3063
3030
  }
3064
- const toRel = (abs) => relative2(vaultRoot2, abs).split(sep2).join("/");
3031
+ const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
3065
3032
  async function upsert(relPath, op) {
3066
3033
  try {
3067
- const raw = await readFile11(join15(vaultRoot2, relPath), "utf8");
3034
+ const raw = await readFile11(join6(vaultRoot2, relPath), "utf8");
3068
3035
  removeNoteBacklinks(ctx2, relPath);
3069
3036
  upsertDoc(ctx2, buildDoc(relPath, raw));
3070
3037
  addNoteBacklinks(ctx2, relPath, raw);
@@ -3096,7 +3063,7 @@ function startWatcher(ctx2, log2, opts) {
3096
3063
  ignored: (p) => {
3097
3064
  const rel = relative2(vaultRoot2, p);
3098
3065
  if (!rel || rel === ".") return false;
3099
- return rel.split(sep2).filter(Boolean).some((seg) => seg.startsWith(".") && seg !== "..");
3066
+ return rel.split(sep3).filter(Boolean).some((seg) => seg.startsWith(".") && seg !== "..");
3100
3067
  }
3101
3068
  });
3102
3069
  watcher2.on("add", onUpsert("add")).on("change", onUpsert("change")).on("unlink", (abs) => {
@@ -3104,9 +3071,9 @@ function startWatcher(ctx2, log2, opts) {
3104
3071
  opts?.onRawEvent?.("unlink", abs, relPath);
3105
3072
  if (relPath.endsWith(".md")) remove(relPath);
3106
3073
  });
3107
- const ready = new Promise((resolve) => {
3074
+ const ready = new Promise((resolve3) => {
3108
3075
  watcher2.once("ready", () => {
3109
- resolve();
3076
+ resolve3();
3110
3077
  });
3111
3078
  });
3112
3079
  return {
@@ -3116,7 +3083,7 @@ function startWatcher(ctx2, log2, opts) {
3116
3083
  }
3117
3084
 
3118
3085
  // src/index.ts
3119
- var VERSION = true ? "0.12.1" : "0.0.0-dev";
3086
+ var VERSION = true ? "0.12.2" : "0.0.0-dev";
3120
3087
  var intent = parseCliIntent(process.argv.slice(2));
3121
3088
  if (intent === "version") {
3122
3089
  process.stdout.write(`${VERSION}
@@ -3146,11 +3113,12 @@ if (intent === "init") {
3146
3113
  }
3147
3114
  var log = createLogger();
3148
3115
  installProcessGuards(log);
3149
- var vaultRoot = process.env.SEEKSTONE_VAULT;
3150
- if (!vaultRoot) {
3116
+ var rawVaultRoot = process.env.SEEKSTONE_VAULT;
3117
+ if (!rawVaultRoot) {
3151
3118
  log.error("SEEKSTONE_VAULT env var is required");
3152
3119
  process.exit(1);
3153
3120
  }
3121
+ var vaultRoot = resolve2(rawVaultRoot);
3154
3122
  var policy = parseWritePolicy(process.env);
3155
3123
  if (policy.readOnly) log.info("read-only mode", { env: "SEEKSTONE_READ_ONLY" });
3156
3124
  if (policy.writeGlobs) log.info("write paths scoped", { globs: policy.writeGlobs });