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/README.md +7 -5
- package/dist/index.js +305 -137
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 =
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
769
|
+
await mkdir2(join(ctx2.vaultRoot, ".trash"), { recursive: true });
|
|
730
770
|
trashedTo = await freeTrashPath(ctx2.vaultRoot, basename2(input.path));
|
|
731
|
-
await rename2(absPath,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
943
|
-
|
|
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 =
|
|
980
|
-
const absTo =
|
|
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(
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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}${
|
|
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 +
|
|
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
|
|
1311
|
-
newContent = `${before}${input.content}${
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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 =
|
|
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 =
|
|
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/
|
|
1779
|
+
// src/tools/rename_heading.ts
|
|
1768
1780
|
import { readFile as readFile8 } from "fs/promises";
|
|
1769
|
-
import { join as
|
|
1781
|
+
import { join as join4 } from "path";
|
|
1770
1782
|
import { z as z17 } from "zod";
|
|
1771
|
-
var
|
|
1772
|
-
path: z17.string().describe("Vault-relative path
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
2210
|
-
import path, { dirname as dirname4, join as
|
|
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
|
|
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(
|
|
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 =
|
|
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 ??
|
|
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
|
|
3055
|
-
import { join as
|
|
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(
|
|
3231
|
+
const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
|
|
3065
3232
|
async function upsert(relPath, op) {
|
|
3066
3233
|
try {
|
|
3067
|
-
const raw = await
|
|
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(
|
|
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((
|
|
3274
|
+
const ready = new Promise((resolve3) => {
|
|
3108
3275
|
watcher2.once("ready", () => {
|
|
3109
|
-
|
|
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.
|
|
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
|
|
3150
|
-
if (!
|
|
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 });
|