seekstone 0.9.1 → 0.10.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 +710 -442
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -10,13 +10,21 @@ import {
|
|
|
10
10
|
|
|
11
11
|
// src/cli-args.ts
|
|
12
12
|
function parseCliIntent(argv) {
|
|
13
|
-
if (argv[0] === "init")
|
|
13
|
+
if (argv[0] === "init") {
|
|
14
|
+
for (const arg of argv.slice(1)) {
|
|
15
|
+
if (arg === "--help" || arg === "-h") return "init-help";
|
|
16
|
+
}
|
|
17
|
+
return "init";
|
|
18
|
+
}
|
|
14
19
|
for (const arg of argv) {
|
|
15
20
|
if (arg === "--version" || arg === "-v") return "version";
|
|
16
21
|
if (arg === "--help" || arg === "-h") return "help";
|
|
17
22
|
}
|
|
18
23
|
return "run";
|
|
19
24
|
}
|
|
25
|
+
var initOptionLines = ` --vault <path> Vault to use (auto-detected from Obsidian if omitted; fallback: $SEEKSTONE_VAULT)
|
|
26
|
+
--client <name> desktop (default) | code | cursor | vscode
|
|
27
|
+
--write Patch the client config in place (backs it up first)`;
|
|
20
28
|
function helpText(version) {
|
|
21
29
|
return `seekstone ${version} \u2014 an Obsidian MCP server (filesystem-direct, low context-tax)
|
|
22
30
|
|
|
@@ -30,9 +38,7 @@ Usage:
|
|
|
30
38
|
seekstone --help Print this help and exit
|
|
31
39
|
|
|
32
40
|
"seekstone init" options:
|
|
33
|
-
|
|
34
|
-
--client <name> desktop (default) | code | cursor | vscode
|
|
35
|
-
--write Patch the client config in place (backs it up first)
|
|
41
|
+
${initOptionLines}
|
|
36
42
|
|
|
37
43
|
Required environment:
|
|
38
44
|
SEEKSTONE_VAULT Absolute path to your Obsidian vault
|
|
@@ -45,6 +51,25 @@ Optional environment:
|
|
|
45
51
|
Add to Claude Code:
|
|
46
52
|
claude mcp add seekstone --env SEEKSTONE_VAULT=/path/to/vault -- npx -y seekstone
|
|
47
53
|
|
|
54
|
+
Docs: https://github.com/shaqmughal/seekstone`;
|
|
55
|
+
}
|
|
56
|
+
function initHelpText(version) {
|
|
57
|
+
return `seekstone ${version} \u2014 seekstone init
|
|
58
|
+
|
|
59
|
+
Validate an Obsidian vault and print (or, with --write, patch) the MCP config
|
|
60
|
+
for the chosen client (Claude Desktop, Claude Code, Cursor, VS Code).
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
seekstone init [options]
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
${initOptionLines}
|
|
67
|
+
--help, -h Print this help and exit
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
seekstone init
|
|
71
|
+
seekstone init --vault "/Users/you/Obsidian/My Vault" --client code --write
|
|
72
|
+
|
|
48
73
|
Docs: https://github.com/shaqmughal/seekstone`;
|
|
49
74
|
}
|
|
50
75
|
|
|
@@ -117,6 +142,37 @@ function parseFrontmatter(text) {
|
|
|
117
142
|
|
|
118
143
|
// src/tools/append_note.ts
|
|
119
144
|
import { z } from "zod";
|
|
145
|
+
|
|
146
|
+
// src/policy.ts
|
|
147
|
+
import picomatch from "picomatch";
|
|
148
|
+
function parseWritePolicy(env) {
|
|
149
|
+
const ro = (env.SEEKSTONE_READ_ONLY ?? "").toLowerCase();
|
|
150
|
+
const readOnly = ro === "1" || ro === "true";
|
|
151
|
+
const globs = (env.SEEKSTONE_WRITE_PATHS ?? "").split(",").map((g) => g.trim()).filter((g) => g.length > 0);
|
|
152
|
+
return globs.length > 0 ? { readOnly, writeGlobs: globs } : { readOnly };
|
|
153
|
+
}
|
|
154
|
+
function normalizeRel(relPath) {
|
|
155
|
+
let p = relPath.replaceAll("\\", "/");
|
|
156
|
+
while (p.startsWith("./")) p = p.slice(2);
|
|
157
|
+
return p;
|
|
158
|
+
}
|
|
159
|
+
function isWritable(policy2, relPath) {
|
|
160
|
+
if (policy2.readOnly) return false;
|
|
161
|
+
if (!policy2.writeGlobs) return true;
|
|
162
|
+
return picomatch(policy2.writeGlobs, { dot: true })(normalizeRel(relPath));
|
|
163
|
+
}
|
|
164
|
+
function assertWritable(policy2, relPath) {
|
|
165
|
+
if (policy2.readOnly) {
|
|
166
|
+
throw new Error("Server is read-only (SEEKSTONE_READ_ONLY=1); write tools are disabled.");
|
|
167
|
+
}
|
|
168
|
+
if (!isWritable(policy2, relPath)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Write not permitted: "${relPath}" is outside SEEKSTONE_WRITE_PATHS (${policy2.writeGlobs?.join(", ")}).`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/tools/append_note.ts
|
|
120
176
|
var AppendNoteInput = z.object({
|
|
121
177
|
path: z.string().describe("Vault-relative path to the note."),
|
|
122
178
|
content: z.string().min(1).describe(
|
|
@@ -128,6 +184,7 @@ async function appendNote(ctx2, input) {
|
|
|
128
184
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
129
185
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
130
186
|
}
|
|
187
|
+
assertWritable(ctx2.policy, input.path);
|
|
131
188
|
const original = await readFile(absPath, "utf8");
|
|
132
189
|
const fm = parseFrontmatter(original);
|
|
133
190
|
const body = fm.body.endsWith("\n") ? fm.body : `${fm.body}
|
|
@@ -232,6 +289,7 @@ async function createNote(ctx2, input) {
|
|
|
232
289
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
233
290
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
234
291
|
}
|
|
292
|
+
assertWritable(ctx2.policy, input.path);
|
|
235
293
|
if (!input.overwrite) {
|
|
236
294
|
try {
|
|
237
295
|
await access(absPath);
|
|
@@ -265,6 +323,7 @@ async function deleteNote(ctx2, input) {
|
|
|
265
323
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
266
324
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
267
325
|
}
|
|
326
|
+
assertWritable(ctx2.policy, input.path);
|
|
268
327
|
await rm(absPath);
|
|
269
328
|
if (ctx2.notes.has(input.path)) {
|
|
270
329
|
ctx2.index.discard(input.path);
|
|
@@ -422,19 +481,149 @@ function listTags(ctx2, input) {
|
|
|
422
481
|
}
|
|
423
482
|
|
|
424
483
|
// src/tools/move_note.ts
|
|
425
|
-
import { access as access2, mkdir as mkdir2, rename } from "fs/promises";
|
|
484
|
+
import { access as access2, mkdir as mkdir2, rename, writeFile as writeFile3 } from "fs/promises";
|
|
426
485
|
import { dirname as dirname2, join as join6 } from "path";
|
|
427
486
|
import { z as z8 } from "zod";
|
|
487
|
+
|
|
488
|
+
// src/index/backlinks.ts
|
|
489
|
+
function removeNoteBacklinks(ctx2, relPath) {
|
|
490
|
+
const oldDoc = ctx2.notes.get(relPath);
|
|
491
|
+
if (oldDoc === void 0) return;
|
|
492
|
+
for (const link of extractLinksWithLines(oldDoc.raw)) {
|
|
493
|
+
const resolved = resolveLink(link.target, ctx2.notes);
|
|
494
|
+
if (resolved === void 0) continue;
|
|
495
|
+
const arr = ctx2.backlinks.get(resolved);
|
|
496
|
+
if (arr === void 0) continue;
|
|
497
|
+
const filtered = arr.filter((r) => r.path !== relPath);
|
|
498
|
+
ctx2.backlinks.set(resolved, filtered);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function addNoteBacklinks(ctx2, relPath, raw) {
|
|
502
|
+
const seen = /* @__PURE__ */ new Set();
|
|
503
|
+
for (const link of extractLinksWithLines(raw)) {
|
|
504
|
+
const resolved = resolveLink(link.target, ctx2.notes);
|
|
505
|
+
if (resolved === void 0) continue;
|
|
506
|
+
const dedupeKey = `${relPath}\0${resolved}`;
|
|
507
|
+
if (seen.has(dedupeKey)) continue;
|
|
508
|
+
seen.add(dedupeKey);
|
|
509
|
+
let arr = ctx2.backlinks.get(resolved);
|
|
510
|
+
if (arr === void 0) {
|
|
511
|
+
arr = [];
|
|
512
|
+
ctx2.backlinks.set(resolved, arr);
|
|
513
|
+
}
|
|
514
|
+
const ref = { path: relPath, line: link.line, linkType: link.linkType };
|
|
515
|
+
arr.push(ref);
|
|
516
|
+
arr.sort((a, b) => a.path.localeCompare(b.path));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/tools/rewrite_links.ts
|
|
521
|
+
import { posix } from "path";
|
|
522
|
+
var WIKI_RE = /(!?)\[\[([^\]|#\n]{1,512})(#[^\]|\n]{1,512})?(\|[^\]\n]{1,512})?\]\]/g;
|
|
523
|
+
var MD_RE = /(!?)\[([^\]\n]*)\]\(([^)\n]+)\)/g;
|
|
524
|
+
var FENCE_RE = /^\s*(```|~~~)/;
|
|
525
|
+
function wikiTargetFor(newPath, postNotes) {
|
|
526
|
+
const noExt = newPath.replace(/\.md$/i, "");
|
|
527
|
+
const base = noExt.split("/").pop() ?? noExt;
|
|
528
|
+
const baseLower = base.toLowerCase();
|
|
529
|
+
let matches2 = 0;
|
|
530
|
+
for (const p of postNotes.keys()) {
|
|
531
|
+
const pBase = (p.replace(/\.md$/i, "").split("/").pop() ?? "").toLowerCase();
|
|
532
|
+
if (pBase === baseLower && ++matches2 > 1) return noExt;
|
|
533
|
+
}
|
|
534
|
+
return base;
|
|
535
|
+
}
|
|
536
|
+
function mdTargetToVaultPath(rawTarget, sourceDir) {
|
|
537
|
+
let t = rawTarget.trim();
|
|
538
|
+
if (t.startsWith("<") && t.endsWith(">")) t = t.slice(1, -1);
|
|
539
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(t)) return void 0;
|
|
540
|
+
if (t.startsWith("#") || t === "") return void 0;
|
|
541
|
+
const hashIdx = t.indexOf("#");
|
|
542
|
+
if (hashIdx !== -1) t = t.slice(0, hashIdx);
|
|
543
|
+
try {
|
|
544
|
+
t = decodeURIComponent(t);
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
const joined = posix.normalize(posix.join(sourceDir, t));
|
|
548
|
+
if (joined.startsWith("..")) return void 0;
|
|
549
|
+
return joined;
|
|
550
|
+
}
|
|
551
|
+
function rewriteNoteLinks(raw, sourcePath, oldPath, newPath, preNotes, postNotes) {
|
|
552
|
+
const sourceDir = posix.dirname(sourcePath);
|
|
553
|
+
const oldNoExt = oldPath.replace(/\.md$/i, "");
|
|
554
|
+
let count = 0;
|
|
555
|
+
const rewriteWikiLine = (line) => line.replace(WIKI_RE, (full, bang, target, frag, alias) => {
|
|
556
|
+
const t = target.trim();
|
|
557
|
+
if (resolveLink(t, postNotes) === newPath) return full;
|
|
558
|
+
if (resolveLink(t, preNotes) !== oldPath) return full;
|
|
559
|
+
count++;
|
|
560
|
+
const next = wikiTargetFor(newPath, postNotes);
|
|
561
|
+
return `${bang}[[${next}${frag ?? ""}${alias ?? ""}]]`;
|
|
562
|
+
});
|
|
563
|
+
const rewriteMdLine = (line) => line.replace(MD_RE, (full, bang, text, rawTarget) => {
|
|
564
|
+
const asRelative = mdTargetToVaultPath(rawTarget, sourceDir);
|
|
565
|
+
const asAbsolute = mdTargetToVaultPath(rawTarget, ".");
|
|
566
|
+
let vaultAbsoluteStyle;
|
|
567
|
+
if (asRelative === oldPath || asRelative === oldNoExt) vaultAbsoluteStyle = false;
|
|
568
|
+
else if (asAbsolute === oldPath || asAbsolute === oldNoExt) vaultAbsoluteStyle = true;
|
|
569
|
+
else return full;
|
|
570
|
+
count++;
|
|
571
|
+
const trimmed = rawTarget.trim();
|
|
572
|
+
const wrapped = trimmed.startsWith("<") && trimmed.endsWith(">");
|
|
573
|
+
const inner = wrapped ? trimmed.slice(1, -1) : trimmed;
|
|
574
|
+
const hashIdx = inner.indexOf("#");
|
|
575
|
+
const fragment = hashIdx === -1 ? "" : inner.slice(hashIdx);
|
|
576
|
+
const hadExt = /\.md(#|$)/i.test(inner);
|
|
577
|
+
let next = vaultAbsoluteStyle ? newPath : posix.relative(sourceDir, newPath);
|
|
578
|
+
if (!hadExt) next = next.replace(/\.md$/i, "");
|
|
579
|
+
if (inner.includes("%20")) next = next.replaceAll(" ", "%20");
|
|
580
|
+
const needsWrap = wrapped || next.includes(" ");
|
|
581
|
+
const target = needsWrap ? `<${next}${fragment}>` : `${next}${fragment}`;
|
|
582
|
+
return `${bang}[${text}](${target})`;
|
|
583
|
+
});
|
|
584
|
+
let inFence = false;
|
|
585
|
+
const lines = raw.split("\n").map((line) => {
|
|
586
|
+
if (FENCE_RE.test(line)) {
|
|
587
|
+
inFence = !inFence;
|
|
588
|
+
return line;
|
|
589
|
+
}
|
|
590
|
+
if (inFence) return line;
|
|
591
|
+
return rewriteMdLine(rewriteWikiLine(line));
|
|
592
|
+
});
|
|
593
|
+
return { content: lines.join("\n"), count };
|
|
594
|
+
}
|
|
595
|
+
function hasMarkdownLinkTo(raw, sourcePath, targetPath) {
|
|
596
|
+
if (!raw.includes("](")) return false;
|
|
597
|
+
const sourceDir = posix.dirname(sourcePath);
|
|
598
|
+
const noExt = targetPath.replace(/\.md$/i, "");
|
|
599
|
+
for (const m of raw.matchAll(MD_RE)) {
|
|
600
|
+
const rawTarget = m[3] ?? "";
|
|
601
|
+
const asRelative = mdTargetToVaultPath(rawTarget, sourceDir);
|
|
602
|
+
const asAbsolute = mdTargetToVaultPath(rawTarget, ".");
|
|
603
|
+
if (asRelative === targetPath || asRelative === noExt || asAbsolute === targetPath || asAbsolute === noExt) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/tools/move_note.ts
|
|
428
611
|
var MoveNoteInput = z8.object({
|
|
429
612
|
from: z8.string().describe("Vault-relative path of the note to move."),
|
|
430
613
|
to: z8.string().describe("Vault-relative destination path."),
|
|
431
|
-
overwrite: z8.boolean().default(false).describe("Overwrite the destination if it exists. Defaults to false.")
|
|
614
|
+
overwrite: z8.boolean().default(false).describe("Overwrite the destination if it exists. Defaults to false."),
|
|
615
|
+
rewriteLinks: z8.boolean().default(true).describe(
|
|
616
|
+
"Rewrite wikilinks and markdown links in other notes that point at the moved note, so nothing breaks. Defaults to true; pass false to move the file only."
|
|
617
|
+
)
|
|
432
618
|
});
|
|
433
|
-
async function moveNote(ctx2,
|
|
619
|
+
async function moveNote(ctx2, rawInput) {
|
|
620
|
+
const input = MoveNoteInput.parse(rawInput);
|
|
434
621
|
const absFrom = join6(ctx2.vaultRoot, input.from);
|
|
435
622
|
const absTo = join6(ctx2.vaultRoot, input.to);
|
|
436
623
|
if (!absFrom.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${input.from}`);
|
|
437
624
|
if (!absTo.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${input.to}`);
|
|
625
|
+
assertWritable(ctx2.policy, input.from);
|
|
626
|
+
assertWritable(ctx2.policy, input.to);
|
|
438
627
|
try {
|
|
439
628
|
await access2(absFrom);
|
|
440
629
|
} catch {
|
|
@@ -450,15 +639,59 @@ async function moveNote(ctx2, input) {
|
|
|
450
639
|
if (err.code !== "ENOENT") throw err;
|
|
451
640
|
}
|
|
452
641
|
}
|
|
642
|
+
const preNotes = new Map(ctx2.notes);
|
|
643
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
644
|
+
if (input.rewriteLinks) {
|
|
645
|
+
for (const ref of ctx2.backlinks.get(input.from) ?? []) candidates.add(ref.path);
|
|
646
|
+
for (const [path2, doc] of preNotes) {
|
|
647
|
+
if (path2 === input.from || candidates.has(path2)) continue;
|
|
648
|
+
if (hasMarkdownLinkTo(doc.raw, path2, input.from)) candidates.add(path2);
|
|
649
|
+
}
|
|
650
|
+
candidates.delete(input.from);
|
|
651
|
+
}
|
|
453
652
|
await mkdir2(dirname2(absTo), { recursive: true });
|
|
454
653
|
await rename(absFrom, absTo);
|
|
455
654
|
const raw = ctx2.notes.get(input.from)?.raw ?? "";
|
|
655
|
+
removeNoteBacklinks(ctx2, input.from);
|
|
456
656
|
if (ctx2.notes.has(input.from)) {
|
|
457
657
|
ctx2.index.discard(input.from);
|
|
458
658
|
ctx2.notes.delete(input.from);
|
|
459
659
|
}
|
|
460
660
|
upsertDoc(ctx2, buildDoc(input.to, raw));
|
|
461
|
-
|
|
661
|
+
addNoteBacklinks(ctx2, input.to, raw);
|
|
662
|
+
let notesRewritten = 0;
|
|
663
|
+
let linksRewritten = 0;
|
|
664
|
+
const skipped = [];
|
|
665
|
+
for (const path2 of [...candidates].sort()) {
|
|
666
|
+
const doc = ctx2.notes.get(path2);
|
|
667
|
+
if (doc === void 0) continue;
|
|
668
|
+
if (!isWritable(ctx2.policy, path2)) {
|
|
669
|
+
skipped.push(path2);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const { content, count } = rewriteNoteLinks(
|
|
673
|
+
doc.raw,
|
|
674
|
+
path2,
|
|
675
|
+
input.from,
|
|
676
|
+
input.to,
|
|
677
|
+
preNotes,
|
|
678
|
+
ctx2.notes
|
|
679
|
+
);
|
|
680
|
+
removeNoteBacklinks(ctx2, path2);
|
|
681
|
+
if (count > 0) {
|
|
682
|
+
await writeFile3(join6(ctx2.vaultRoot, path2), content, "utf8");
|
|
683
|
+
upsertDoc(ctx2, buildDoc(path2, content));
|
|
684
|
+
notesRewritten++;
|
|
685
|
+
linksRewritten += count;
|
|
686
|
+
addNoteBacklinks(ctx2, path2, content);
|
|
687
|
+
} else {
|
|
688
|
+
addNoteBacklinks(ctx2, path2, doc.raw);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
ctx2.backlinks.delete(input.from);
|
|
692
|
+
const result = { from: input.from, to: input.to, notesRewritten, linksRewritten };
|
|
693
|
+
if (skipped.length > 0) result.skipped = skipped;
|
|
694
|
+
return result;
|
|
462
695
|
}
|
|
463
696
|
|
|
464
697
|
// src/tools/outline_note.ts
|
|
@@ -532,7 +765,7 @@ async function outlineNote(ctx2, input) {
|
|
|
532
765
|
}
|
|
533
766
|
|
|
534
767
|
// src/tools/patch_frontmatter.ts
|
|
535
|
-
import { readFile as readFile3, writeFile as
|
|
768
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
536
769
|
import { join as join8 } from "path";
|
|
537
770
|
import { parseDocument } from "yaml";
|
|
538
771
|
import { z as z10 } from "zod";
|
|
@@ -547,6 +780,7 @@ async function patchFrontmatter(ctx2, input) {
|
|
|
547
780
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
548
781
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
549
782
|
}
|
|
783
|
+
assertWritable(ctx2.policy, input.path);
|
|
550
784
|
const original = await readFile3(absPath, "utf8");
|
|
551
785
|
const fm = parseFrontmatter(original);
|
|
552
786
|
const keysChanged = [];
|
|
@@ -589,7 +823,7 @@ ${original}`;
|
|
|
589
823
|
const tail = opensWithCRLF ? "---\r\n" : "---\n";
|
|
590
824
|
newContent = `${head}${doc.toString()}${tail}${fm.body}`;
|
|
591
825
|
}
|
|
592
|
-
await
|
|
826
|
+
await writeFile4(absPath, newContent, "utf8");
|
|
593
827
|
const cached = ctx2.notes.get(input.path);
|
|
594
828
|
if (cached) {
|
|
595
829
|
const updated = parseFrontmatter(newContent);
|
|
@@ -600,7 +834,7 @@ ${original}`;
|
|
|
600
834
|
}
|
|
601
835
|
|
|
602
836
|
// src/tools/patch_note.ts
|
|
603
|
-
import { readFile as readFile4, rename as rename2, writeFile as
|
|
837
|
+
import { readFile as readFile4, rename as rename2, writeFile as writeFile5 } from "fs/promises";
|
|
604
838
|
import { join as join9 } from "path";
|
|
605
839
|
import { z as z11 } from "zod";
|
|
606
840
|
var PatchNoteInput = z11.object({
|
|
@@ -628,7 +862,7 @@ function lineStart(raw, offset) {
|
|
|
628
862
|
}
|
|
629
863
|
async function atomicWrite(absPath, content) {
|
|
630
864
|
const tmpPath = `${absPath}.seekstone-patch-tmp`;
|
|
631
|
-
await
|
|
865
|
+
await writeFile5(tmpPath, content, "utf8");
|
|
632
866
|
await rename2(tmpPath, absPath);
|
|
633
867
|
}
|
|
634
868
|
async function patchNote(ctx2, input) {
|
|
@@ -636,6 +870,7 @@ async function patchNote(ctx2, input) {
|
|
|
636
870
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
637
871
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
638
872
|
}
|
|
873
|
+
assertWritable(ctx2.policy, input.path);
|
|
639
874
|
const raw = await readFile4(absPath, "utf8");
|
|
640
875
|
const fm = parseFrontmatter(raw);
|
|
641
876
|
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
@@ -748,7 +983,7 @@ function updateCache(ctx2, path2, newContent, bodyStart) {
|
|
|
748
983
|
}
|
|
749
984
|
|
|
750
985
|
// src/tools/periodic_note.ts
|
|
751
|
-
import { access as access3, mkdir as mkdir3, readFile as readFile5, writeFile as
|
|
986
|
+
import { access as access3, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
|
|
752
987
|
import { dirname as dirname3, join as join10 } from "path";
|
|
753
988
|
import { z as z12 } from "zod";
|
|
754
989
|
var PERIOD = z12.enum(["daily", "weekly", "monthly", "quarterly", "yearly"]);
|
|
@@ -840,6 +1075,7 @@ async function getPeriodicNote(ctx2, rawInput) {
|
|
|
840
1075
|
}
|
|
841
1076
|
if (existed) return { path: path2, existed: true, created: false };
|
|
842
1077
|
if (!input.createIfMissing) return { path: path2, existed: false, created: false };
|
|
1078
|
+
assertWritable(ctx2.policy, path2);
|
|
843
1079
|
let body = "";
|
|
844
1080
|
if (cfg.template) {
|
|
845
1081
|
const templateAbs = join10(
|
|
@@ -854,7 +1090,7 @@ async function getPeriodicNote(ctx2, rawInput) {
|
|
|
854
1090
|
}
|
|
855
1091
|
}
|
|
856
1092
|
await mkdir3(dirname3(abs), { recursive: true });
|
|
857
|
-
await
|
|
1093
|
+
await writeFile6(abs, body, "utf8");
|
|
858
1094
|
upsertDoc(ctx2, buildDoc(path2, body));
|
|
859
1095
|
return { path: path2, existed: false, created: true };
|
|
860
1096
|
}
|
|
@@ -871,6 +1107,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
871
1107
|
const path2 = resolveNotePath(cfg, date);
|
|
872
1108
|
const abs = join10(ctx2.vaultRoot, path2);
|
|
873
1109
|
if (!abs.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${path2}`);
|
|
1110
|
+
assertWritable(ctx2.policy, path2);
|
|
874
1111
|
let original = "";
|
|
875
1112
|
try {
|
|
876
1113
|
original = await readFile5(abs, "utf8");
|
|
@@ -878,7 +1115,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
878
1115
|
if (err.code !== "ENOENT") throw err;
|
|
879
1116
|
if (!input.createIfMissing) throw new Error(`Periodic note not found: ${path2}`);
|
|
880
1117
|
await mkdir3(dirname3(abs), { recursive: true });
|
|
881
|
-
await
|
|
1118
|
+
await writeFile6(abs, input.content, "utf8");
|
|
882
1119
|
upsertDoc(ctx2, buildDoc(path2, input.content));
|
|
883
1120
|
return { path: path2, bytesWritten: Buffer.byteLength(input.content, "utf8") };
|
|
884
1121
|
}
|
|
@@ -889,7 +1126,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
889
1126
|
const newBody = `${body}${separator}${input.content}`;
|
|
890
1127
|
const header = original.slice(0, fm.bodyStart);
|
|
891
1128
|
const newRaw = `${header}${newBody}`;
|
|
892
|
-
await
|
|
1129
|
+
await writeFile6(abs, newRaw, "utf8");
|
|
893
1130
|
const cached = ctx2.notes.get(path2);
|
|
894
1131
|
if (cached) {
|
|
895
1132
|
cached.body = newBody;
|
|
@@ -1206,7 +1443,7 @@ async function readNote(ctx2, input) {
|
|
|
1206
1443
|
}
|
|
1207
1444
|
|
|
1208
1445
|
// src/tools/replace_in_note.ts
|
|
1209
|
-
import { readFile as readFile7, rename as rename3, writeFile as
|
|
1446
|
+
import { readFile as readFile7, rename as rename3, writeFile as writeFile7 } from "fs/promises";
|
|
1210
1447
|
import { join as join12 } from "path";
|
|
1211
1448
|
import { z as z16 } from "zod";
|
|
1212
1449
|
var ReplaceInNoteInput = z16.object({
|
|
@@ -1230,7 +1467,7 @@ function buildRegex(find, opts) {
|
|
|
1230
1467
|
}
|
|
1231
1468
|
async function atomicWrite2(absPath, content) {
|
|
1232
1469
|
const tmpPath = `${absPath}.seekstone-replace-tmp`;
|
|
1233
|
-
await
|
|
1470
|
+
await writeFile7(tmpPath, content, "utf8");
|
|
1234
1471
|
await rename3(tmpPath, absPath);
|
|
1235
1472
|
}
|
|
1236
1473
|
async function replaceInNote(ctx2, input) {
|
|
@@ -1238,6 +1475,7 @@ async function replaceInNote(ctx2, input) {
|
|
|
1238
1475
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
1239
1476
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
1240
1477
|
}
|
|
1478
|
+
assertWritable(ctx2.policy, input.path);
|
|
1241
1479
|
let re;
|
|
1242
1480
|
try {
|
|
1243
1481
|
re = buildRegex(input.find, {
|
|
@@ -1299,6 +1537,16 @@ async function replaceInNote(ctx2, input) {
|
|
|
1299
1537
|
}
|
|
1300
1538
|
|
|
1301
1539
|
// src/dispatch.ts
|
|
1540
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
1541
|
+
"create_note",
|
|
1542
|
+
"delete_note",
|
|
1543
|
+
"move_note",
|
|
1544
|
+
"append_note",
|
|
1545
|
+
"patch_frontmatter",
|
|
1546
|
+
"patch_note",
|
|
1547
|
+
"replace_in_note",
|
|
1548
|
+
"append_periodic_note"
|
|
1549
|
+
]);
|
|
1302
1550
|
var META_KEYS = [
|
|
1303
1551
|
"path",
|
|
1304
1552
|
"from",
|
|
@@ -1339,6 +1587,23 @@ async function dispatch(ctx2, name, args, log2) {
|
|
|
1339
1587
|
const start = performance.now();
|
|
1340
1588
|
const meta = safeMeta(args);
|
|
1341
1589
|
log2.debug("tool start", { tool: name, args });
|
|
1590
|
+
if (ctx2.policy.readOnly && WRITE_TOOLS.has(name)) {
|
|
1591
|
+
log2.info("tool rejected (read-only)", { tool: name, ...meta });
|
|
1592
|
+
return {
|
|
1593
|
+
content: [
|
|
1594
|
+
{
|
|
1595
|
+
type: "text",
|
|
1596
|
+
text: "Error: Server is read-only (SEEKSTONE_READ_ONLY=1); write tools are disabled."
|
|
1597
|
+
}
|
|
1598
|
+
],
|
|
1599
|
+
isError: true
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
if (ctx2.policy.readOnly && name === "get_periodic_note") {
|
|
1603
|
+
if (typeof args === "object" && args !== null) {
|
|
1604
|
+
args = { ...args, createIfMissing: false };
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1342
1607
|
try {
|
|
1343
1608
|
const result = await run(ctx2, name, args);
|
|
1344
1609
|
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
|
@@ -1399,7 +1664,11 @@ async function run(ctx2, name, args) {
|
|
|
1399
1664
|
case "move_note": {
|
|
1400
1665
|
const input = MoveNoteInput.parse(args);
|
|
1401
1666
|
const result = await moveNote(ctx2, input);
|
|
1402
|
-
|
|
1667
|
+
let text = `Moved ${result.from} \u2192 ${result.to}. Rewrote ${result.linksRewritten} link${result.linksRewritten === 1 ? "" : "s"} in ${result.notesRewritten} note${result.notesRewritten === 1 ? "" : "s"}.`;
|
|
1668
|
+
if (result.skipped?.length) {
|
|
1669
|
+
text += ` Skipped (outside SEEKSTONE_WRITE_PATHS): ${result.skipped.join(", ")}.`;
|
|
1670
|
+
}
|
|
1671
|
+
return { content: [{ type: "text", text }] };
|
|
1403
1672
|
}
|
|
1404
1673
|
case "append_note": {
|
|
1405
1674
|
const input = AppendNoteInput.parse(args);
|
|
@@ -1598,7 +1867,7 @@ function buildBacklinks(notes2) {
|
|
|
1598
1867
|
|
|
1599
1868
|
// src/init.ts
|
|
1600
1869
|
import { execFileSync } from "child_process";
|
|
1601
|
-
import { access as access4, copyFile, mkdir as mkdir4, readdir, readFile as readFile9, stat as stat2, writeFile as
|
|
1870
|
+
import { access as access4, copyFile, mkdir as mkdir4, readdir, readFile as readFile9, stat as stat2, writeFile as writeFile8 } from "fs/promises";
|
|
1602
1871
|
import path, { dirname as dirname4, join as join14 } from "path";
|
|
1603
1872
|
function parseInitArgs(argv) {
|
|
1604
1873
|
const opts = { write: false, client: "desktop" };
|
|
@@ -1609,6 +1878,7 @@ function parseInitArgs(argv) {
|
|
|
1609
1878
|
else if (a === "--client") {
|
|
1610
1879
|
const v = argv[++i];
|
|
1611
1880
|
if (v === "desktop" || v === "code" || v === "cursor" || v === "vscode") opts.client = v;
|
|
1881
|
+
else opts.invalidClient = v ?? "(missing)";
|
|
1612
1882
|
}
|
|
1613
1883
|
}
|
|
1614
1884
|
return opts;
|
|
@@ -1732,6 +2002,15 @@ async function countMarkdown(root) {
|
|
|
1732
2002
|
return count;
|
|
1733
2003
|
}
|
|
1734
2004
|
async function runInit(opts, deps) {
|
|
2005
|
+
if (opts.invalidClient !== void 0) {
|
|
2006
|
+
return {
|
|
2007
|
+
ok: false,
|
|
2008
|
+
exitCode: 1,
|
|
2009
|
+
output: [
|
|
2010
|
+
`\u2717 Unknown --client: ${opts.invalidClient}. Known clients: desktop, code, cursor, vscode.`
|
|
2011
|
+
]
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
1735
2014
|
const rf = deps.readFile ?? readFile9;
|
|
1736
2015
|
let vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT;
|
|
1737
2016
|
if (!vaultPath) {
|
|
@@ -1868,7 +2147,7 @@ async function runInit(opts, deps) {
|
|
|
1868
2147
|
backupPath = `${configPath}.backup-${deps.timestamp}`;
|
|
1869
2148
|
await copyFile(configPath, backupPath);
|
|
1870
2149
|
}
|
|
1871
|
-
await
|
|
2150
|
+
await writeFile8(configPath, `${JSON.stringify(merged, null, 2)}
|
|
1872
2151
|
`, "utf8");
|
|
1873
2152
|
out.push(
|
|
1874
2153
|
`\u2713 Patched ${configPath}`,
|
|
@@ -1939,7 +2218,7 @@ function createLogger(opts = {}) {
|
|
|
1939
2218
|
);
|
|
1940
2219
|
}
|
|
1941
2220
|
}
|
|
1942
|
-
function
|
|
2221
|
+
function writeFile9(record) {
|
|
1943
2222
|
if (!fileOk || filePath === void 0) return;
|
|
1944
2223
|
const line = `${JSON.stringify(record)}
|
|
1945
2224
|
`;
|
|
@@ -1961,7 +2240,7 @@ function createLogger(opts = {}) {
|
|
|
1961
2240
|
const f = fields ?? {};
|
|
1962
2241
|
stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
|
|
1963
2242
|
`);
|
|
1964
|
-
|
|
2243
|
+
writeFile9({ t: ts, level: lvl, msg, ...f });
|
|
1965
2244
|
}
|
|
1966
2245
|
return {
|
|
1967
2246
|
level,
|
|
@@ -1992,41 +2271,407 @@ function installProcessGuards(log2) {
|
|
|
1992
2271
|
};
|
|
1993
2272
|
}
|
|
1994
2273
|
|
|
2274
|
+
// src/tool-list.ts
|
|
2275
|
+
var ALL_TOOLS = [
|
|
2276
|
+
{
|
|
2277
|
+
name: "search",
|
|
2278
|
+
description: "Full-text search across the vault. Returns ranked excerpts (~200 chars) \u2014 not full notes \u2014 to minimise context usage. Supports fuzzy matching and prefix search.",
|
|
2279
|
+
inputSchema: {
|
|
2280
|
+
type: "object",
|
|
2281
|
+
properties: {
|
|
2282
|
+
query: { type: "string", description: "Search query." },
|
|
2283
|
+
limit: { type: "number", description: "Max results (1\u201350, default 10)." },
|
|
2284
|
+
folder: { type: "string", description: "Restrict to a vault-relative folder prefix." },
|
|
2285
|
+
tag: { type: "string", description: "Restrict to notes with this tag." }
|
|
2286
|
+
},
|
|
2287
|
+
required: ["query"]
|
|
2288
|
+
}
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
name: "query_notes",
|
|
2292
|
+
description: "Structured metadata query \u2014 filter notes by frontmatter key/value predicates, tag, folder, modified time, and size. Returns compact rows (path + title by default; opt into more via select), not note content. Use this instead of search when filtering by properties rather than text.",
|
|
2293
|
+
inputSchema: {
|
|
2294
|
+
type: "object",
|
|
2295
|
+
properties: {
|
|
2296
|
+
where: {
|
|
2297
|
+
type: "array",
|
|
2298
|
+
description: "Frontmatter predicates \u2014 all must match (AND).",
|
|
2299
|
+
items: {
|
|
2300
|
+
type: "object",
|
|
2301
|
+
properties: {
|
|
2302
|
+
key: { type: "string", description: "Frontmatter key to test." },
|
|
2303
|
+
op: {
|
|
2304
|
+
type: "string",
|
|
2305
|
+
enum: ["eq", "ne", "contains", "exists", "missing", "gt", "gte", "lt", "lte"],
|
|
2306
|
+
description: "eq/ne compare scalars; contains matches array membership or substring; exists/missing test key presence; gt/gte/lt/lte compare numbers or strings (ISO dates sort correctly)."
|
|
2307
|
+
},
|
|
2308
|
+
value: {
|
|
2309
|
+
description: "Comparison value. Required for every op except exists/missing."
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
required: ["key", "op"]
|
|
2313
|
+
}
|
|
2314
|
+
},
|
|
2315
|
+
folder: { type: "string", description: "Restrict to a vault-relative folder prefix." },
|
|
2316
|
+
tag: { type: "string", description: "Restrict to notes with this tag (# optional)." },
|
|
2317
|
+
modifiedAfter: {
|
|
2318
|
+
type: "string",
|
|
2319
|
+
description: "Only notes modified at or after this ISO 8601 date/time."
|
|
2320
|
+
},
|
|
2321
|
+
modifiedBefore: {
|
|
2322
|
+
type: "string",
|
|
2323
|
+
description: "Only notes modified before this ISO 8601 date/time."
|
|
2324
|
+
},
|
|
2325
|
+
minSizeBytes: { type: "number", description: "Only notes at least this many bytes." },
|
|
2326
|
+
maxSizeBytes: { type: "number", description: "Only notes at most this many bytes." },
|
|
2327
|
+
select: {
|
|
2328
|
+
type: "array",
|
|
2329
|
+
items: { type: "string" },
|
|
2330
|
+
description: 'Extra fields per hit: frontmatter keys, or "mtime", "size", "tags". Default returns only path + title.'
|
|
2331
|
+
},
|
|
2332
|
+
sort: {
|
|
2333
|
+
type: "string",
|
|
2334
|
+
enum: ["path", "title", "mtime", "size"],
|
|
2335
|
+
description: "Sort field (default path)."
|
|
2336
|
+
},
|
|
2337
|
+
order: {
|
|
2338
|
+
type: "string",
|
|
2339
|
+
enum: ["asc", "desc"],
|
|
2340
|
+
description: "Sort order (default asc)."
|
|
2341
|
+
},
|
|
2342
|
+
limit: { type: "number", description: "Max results (1\u2013500, default 100)." }
|
|
2343
|
+
},
|
|
2344
|
+
required: []
|
|
2345
|
+
}
|
|
2346
|
+
},
|
|
2347
|
+
{
|
|
2348
|
+
name: "read_note",
|
|
2349
|
+
description: "Read a note or a span of it \u2014 by heading section, block reference, or line range. Returns structured JSON with the content, bytes returned, and total note size so payload savings are measurable. Use search or outline_note first to find the right path and section names.",
|
|
2350
|
+
inputSchema: {
|
|
2351
|
+
type: "object",
|
|
2352
|
+
properties: {
|
|
2353
|
+
path: {
|
|
2354
|
+
type: "string",
|
|
2355
|
+
description: 'Vault-relative path, e.g. "Daily Notes/2026-05-29.md".'
|
|
2356
|
+
},
|
|
2357
|
+
section: {
|
|
2358
|
+
type: "string",
|
|
2359
|
+
description: "Return only this heading section (heading text, # prefix optional). First match wins."
|
|
2360
|
+
},
|
|
2361
|
+
block: {
|
|
2362
|
+
type: "string",
|
|
2363
|
+
description: "Return only the line anchored by this block id (^ prefix optional)."
|
|
2364
|
+
},
|
|
2365
|
+
lines: {
|
|
2366
|
+
type: "object",
|
|
2367
|
+
description: "Return only this line range (1-indexed, inclusive).",
|
|
2368
|
+
properties: {
|
|
2369
|
+
from: { type: "number", description: "First line (1-indexed)." },
|
|
2370
|
+
to: {
|
|
2371
|
+
type: "number",
|
|
2372
|
+
description: "Last line (1-indexed, inclusive). Defaults to EOF."
|
|
2373
|
+
}
|
|
2374
|
+
},
|
|
2375
|
+
required: ["from"]
|
|
2376
|
+
},
|
|
2377
|
+
includeFrontmatter: {
|
|
2378
|
+
type: "boolean",
|
|
2379
|
+
description: "Prepend frontmatter to section/block span results. Default false for spans."
|
|
2380
|
+
}
|
|
2381
|
+
},
|
|
2382
|
+
required: ["path"]
|
|
2383
|
+
}
|
|
2384
|
+
},
|
|
2385
|
+
{
|
|
2386
|
+
name: "list_notes",
|
|
2387
|
+
description: "List notes, optionally filtered by folder prefix or tag.",
|
|
2388
|
+
inputSchema: {
|
|
2389
|
+
type: "object",
|
|
2390
|
+
properties: {
|
|
2391
|
+
folder: { type: "string", description: "Vault-relative folder prefix." },
|
|
2392
|
+
tag: { type: "string", description: "Filter by tag (# prefix optional)." },
|
|
2393
|
+
limit: { type: "number", description: "Max results (1\u2013500, default 100)." }
|
|
2394
|
+
},
|
|
2395
|
+
required: []
|
|
2396
|
+
}
|
|
2397
|
+
},
|
|
2398
|
+
{
|
|
2399
|
+
name: "list_tags",
|
|
2400
|
+
description: "List all tags in the vault with usage counts. Supports substring filtering, minimum count threshold, and sort order. Nested tags (e.g. area/work) include a parent field.",
|
|
2401
|
+
inputSchema: {
|
|
2402
|
+
type: "object",
|
|
2403
|
+
properties: {
|
|
2404
|
+
pattern: {
|
|
2405
|
+
type: "string",
|
|
2406
|
+
description: 'Substring filter on tag name. Example: "work" matches "work" and "area/work".'
|
|
2407
|
+
},
|
|
2408
|
+
minCount: {
|
|
2409
|
+
type: "number",
|
|
2410
|
+
description: "Only return tags used in at least this many notes."
|
|
2411
|
+
},
|
|
2412
|
+
sort: {
|
|
2413
|
+
type: "string",
|
|
2414
|
+
enum: ["count", "name"],
|
|
2415
|
+
description: "Sort by usage count descending (default) or alphabetically."
|
|
2416
|
+
}
|
|
2417
|
+
},
|
|
2418
|
+
required: []
|
|
2419
|
+
}
|
|
2420
|
+
},
|
|
2421
|
+
{
|
|
2422
|
+
name: "create_note",
|
|
2423
|
+
description: "Create a new note at a vault-relative path. Optionally sets frontmatter and body content. Parent directories are created automatically. Fails if the note already exists unless overwrite is true.",
|
|
2424
|
+
inputSchema: {
|
|
2425
|
+
type: "object",
|
|
2426
|
+
properties: {
|
|
2427
|
+
path: {
|
|
2428
|
+
type: "string",
|
|
2429
|
+
description: 'Vault-relative path, e.g. "Daily Notes/2026-06-01.md".'
|
|
2430
|
+
},
|
|
2431
|
+
content: { type: "string", description: "Body content for the note." },
|
|
2432
|
+
frontmatter: {
|
|
2433
|
+
type: "object",
|
|
2434
|
+
description: "Frontmatter key-value pairs.",
|
|
2435
|
+
additionalProperties: true
|
|
2436
|
+
},
|
|
2437
|
+
overwrite: {
|
|
2438
|
+
type: "boolean",
|
|
2439
|
+
description: "Overwrite an existing note. Defaults to false."
|
|
2440
|
+
}
|
|
2441
|
+
},
|
|
2442
|
+
required: ["path"]
|
|
2443
|
+
}
|
|
2444
|
+
},
|
|
2445
|
+
{
|
|
2446
|
+
name: "delete_note",
|
|
2447
|
+
description: "Permanently delete a note from the vault. This cannot be undone.",
|
|
2448
|
+
inputSchema: {
|
|
2449
|
+
type: "object",
|
|
2450
|
+
properties: {
|
|
2451
|
+
path: { type: "string", description: "Vault-relative path of the note to delete." }
|
|
2452
|
+
},
|
|
2453
|
+
required: ["path"]
|
|
2454
|
+
}
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
name: "move_note",
|
|
2458
|
+
description: "Move or rename a note to a new vault-relative path, rewriting wikilinks and markdown links in other notes that point at it so nothing breaks (links inside fenced code blocks are left alone). Parent directories at the destination are created automatically. Fails if the destination already exists unless overwrite is true.",
|
|
2459
|
+
inputSchema: {
|
|
2460
|
+
type: "object",
|
|
2461
|
+
properties: {
|
|
2462
|
+
from: { type: "string", description: "Vault-relative source path." },
|
|
2463
|
+
to: { type: "string", description: "Vault-relative destination path." },
|
|
2464
|
+
overwrite: {
|
|
2465
|
+
type: "boolean",
|
|
2466
|
+
description: "Overwrite destination if it exists. Defaults to false."
|
|
2467
|
+
},
|
|
2468
|
+
rewriteLinks: {
|
|
2469
|
+
type: "boolean",
|
|
2470
|
+
description: "Rewrite references in other notes to follow the move. Defaults to true; pass false to move the file only."
|
|
2471
|
+
}
|
|
2472
|
+
},
|
|
2473
|
+
required: ["from", "to"]
|
|
2474
|
+
}
|
|
2475
|
+
},
|
|
2476
|
+
{
|
|
2477
|
+
name: "append_note",
|
|
2478
|
+
description: "Append text to a note body without touching the frontmatter. Safe for meeting notes, daily logs, and append-only workflows.",
|
|
2479
|
+
inputSchema: {
|
|
2480
|
+
type: "object",
|
|
2481
|
+
properties: {
|
|
2482
|
+
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2483
|
+
content: { type: "string", description: "Text to append." }
|
|
2484
|
+
},
|
|
2485
|
+
required: ["path", "content"]
|
|
2486
|
+
}
|
|
2487
|
+
},
|
|
2488
|
+
{
|
|
2489
|
+
name: "patch_frontmatter",
|
|
2490
|
+
description: "Set, update, or delete frontmatter keys without reordering existing keys or changing quote style. Pass null as a value to delete a key.",
|
|
2491
|
+
inputSchema: {
|
|
2492
|
+
type: "object",
|
|
2493
|
+
properties: {
|
|
2494
|
+
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2495
|
+
patch: {
|
|
2496
|
+
type: "object",
|
|
2497
|
+
description: "Key-value pairs to set. Null value removes the key.",
|
|
2498
|
+
additionalProperties: true
|
|
2499
|
+
}
|
|
2500
|
+
},
|
|
2501
|
+
required: ["path", "patch"]
|
|
2502
|
+
}
|
|
2503
|
+
},
|
|
2504
|
+
{
|
|
2505
|
+
name: "outline_note",
|
|
2506
|
+
description: "Return a note's structure \u2014 heading tree with offsets, block-reference anchors, and frontmatter key list \u2014 without returning any prose. Use this before section reads or patches to discover what sections exist at a fraction of the cost of reading the full note.",
|
|
2507
|
+
inputSchema: {
|
|
2508
|
+
type: "object",
|
|
2509
|
+
properties: {
|
|
2510
|
+
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2511
|
+
includeBlocks: {
|
|
2512
|
+
type: "boolean",
|
|
2513
|
+
description: "Include block-reference anchors (^id). Default true."
|
|
2514
|
+
},
|
|
2515
|
+
includeSizes: {
|
|
2516
|
+
type: "boolean",
|
|
2517
|
+
description: "Include per-section character length in each heading entry. Default false."
|
|
2518
|
+
}
|
|
2519
|
+
},
|
|
2520
|
+
required: ["path"]
|
|
2521
|
+
}
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
name: "patch_note",
|
|
2525
|
+
description: "Surgically edit a section of a note \u2014 targeted by heading or block reference \u2014 without rewriting the whole file. Operations: append (add after section), prepend (add after heading line), replace (swap section content). Frontmatter is never touched.",
|
|
2526
|
+
inputSchema: {
|
|
2527
|
+
type: "object",
|
|
2528
|
+
properties: {
|
|
2529
|
+
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2530
|
+
target: {
|
|
2531
|
+
type: "object",
|
|
2532
|
+
description: 'Exactly one of: { heading: "Section Title" } or { block: "block-id" } (without the ^ prefix).'
|
|
2533
|
+
},
|
|
2534
|
+
operation: {
|
|
2535
|
+
type: "string",
|
|
2536
|
+
enum: ["append", "prepend", "replace"],
|
|
2537
|
+
description: "append: insert after section content. prepend: insert after heading line. replace: swap section content."
|
|
2538
|
+
},
|
|
2539
|
+
content: { type: "string", description: "Content to insert or replace with." },
|
|
2540
|
+
createIfMissing: {
|
|
2541
|
+
type: "boolean",
|
|
2542
|
+
description: "If the heading target is not found, append a new heading (level 2) + content. Only valid for heading targets. Default false."
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
required: ["path", "target", "operation", "content"]
|
|
2546
|
+
}
|
|
2547
|
+
},
|
|
2548
|
+
{
|
|
2549
|
+
name: "get_backlinks",
|
|
2550
|
+
description: "Return every note that links to the target note, with the source line and an optional excerpt. Results come from the pre-built reverse-link index so this is a fast, pure index lookup. Sort order: source path ascending.",
|
|
2551
|
+
inputSchema: {
|
|
2552
|
+
type: "object",
|
|
2553
|
+
properties: {
|
|
2554
|
+
path: { type: "string", description: "Vault-relative path to the target note." },
|
|
2555
|
+
includeContext: {
|
|
2556
|
+
type: "boolean",
|
|
2557
|
+
description: "Include a short excerpt (~200 chars) from the linking line. Default true."
|
|
2558
|
+
},
|
|
2559
|
+
limit: {
|
|
2560
|
+
type: "number",
|
|
2561
|
+
description: "Max backlinks to return (1\u2013500). Default 50."
|
|
2562
|
+
}
|
|
2563
|
+
},
|
|
2564
|
+
required: ["path"]
|
|
2565
|
+
}
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
name: "get_links",
|
|
2569
|
+
description: "Return all outgoing wikilinks and embeds from a note. Each link is marked resolved (with target path) or unresolved. Duplicate targets are de-duplicated; results sorted by line number.",
|
|
2570
|
+
inputSchema: {
|
|
2571
|
+
type: "object",
|
|
2572
|
+
properties: {
|
|
2573
|
+
path: { type: "string", description: "Vault-relative path to the note." }
|
|
2574
|
+
},
|
|
2575
|
+
required: ["path"]
|
|
2576
|
+
}
|
|
2577
|
+
},
|
|
2578
|
+
{
|
|
2579
|
+
name: "replace_in_note",
|
|
2580
|
+
description: "Find and replace text within a note body. Supports literal and regex search, case sensitivity, whole-word matching, and a replacement limit. Frontmatter is never touched. Use dryRun to preview matches before writing.",
|
|
2581
|
+
inputSchema: {
|
|
2582
|
+
type: "object",
|
|
2583
|
+
properties: {
|
|
2584
|
+
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2585
|
+
find: { type: "string", description: "Text or pattern to find." },
|
|
2586
|
+
replace: {
|
|
2587
|
+
type: "string",
|
|
2588
|
+
description: "Replacement text. Supports $1, $2, \u2026 backreferences in regex mode."
|
|
2589
|
+
},
|
|
2590
|
+
regex: {
|
|
2591
|
+
type: "boolean",
|
|
2592
|
+
description: "Treat find as a regular expression. Default false."
|
|
2593
|
+
},
|
|
2594
|
+
caseSensitive: {
|
|
2595
|
+
type: "boolean",
|
|
2596
|
+
description: "Case-sensitive matching. Default false."
|
|
2597
|
+
},
|
|
2598
|
+
wholeWord: {
|
|
2599
|
+
type: "boolean",
|
|
2600
|
+
description: "Match whole words only (\\b boundary). Default false."
|
|
2601
|
+
},
|
|
2602
|
+
limit: {
|
|
2603
|
+
type: "number",
|
|
2604
|
+
description: "Maximum number of replacements. Omit to replace all."
|
|
2605
|
+
},
|
|
2606
|
+
dryRun: {
|
|
2607
|
+
type: "boolean",
|
|
2608
|
+
description: "If true, report matches without writing. Default false."
|
|
2609
|
+
}
|
|
2610
|
+
},
|
|
2611
|
+
required: ["path", "find", "replace"]
|
|
2612
|
+
}
|
|
2613
|
+
},
|
|
2614
|
+
{
|
|
2615
|
+
name: "get_periodic_note",
|
|
2616
|
+
description: "Get the path and existence status of a periodic note (daily, weekly, monthly, quarterly, or yearly) for a given date. Reads folder/format config from .obsidian/daily-notes.json (daily) or the periodic-notes plugin data.json. Optionally creates the note from the configured template if it is missing.",
|
|
2617
|
+
inputSchema: {
|
|
2618
|
+
type: "object",
|
|
2619
|
+
properties: {
|
|
2620
|
+
period: {
|
|
2621
|
+
type: "string",
|
|
2622
|
+
enum: ["daily", "weekly", "monthly", "quarterly", "yearly"],
|
|
2623
|
+
description: "Period type. Default: daily."
|
|
2624
|
+
},
|
|
2625
|
+
date: {
|
|
2626
|
+
type: "string",
|
|
2627
|
+
description: "ISO date string (YYYY-MM-DD). Defaults to today when omitted."
|
|
2628
|
+
},
|
|
2629
|
+
createIfMissing: {
|
|
2630
|
+
type: "boolean",
|
|
2631
|
+
description: "Create the note from the configured template if it does not exist. Default false."
|
|
2632
|
+
}
|
|
2633
|
+
},
|
|
2634
|
+
required: []
|
|
2635
|
+
}
|
|
2636
|
+
},
|
|
2637
|
+
{
|
|
2638
|
+
name: "append_periodic_note",
|
|
2639
|
+
description: "Append text to a periodic note (daily, weekly, monthly, quarterly, or yearly). Preserves existing frontmatter exactly. Creates the note first (from template if configured) when createIfMissing is true (default).",
|
|
2640
|
+
inputSchema: {
|
|
2641
|
+
type: "object",
|
|
2642
|
+
properties: {
|
|
2643
|
+
period: {
|
|
2644
|
+
type: "string",
|
|
2645
|
+
enum: ["daily", "weekly", "monthly", "quarterly", "yearly"],
|
|
2646
|
+
description: "Period type. Default: daily."
|
|
2647
|
+
},
|
|
2648
|
+
date: {
|
|
2649
|
+
type: "string",
|
|
2650
|
+
description: "ISO date string (YYYY-MM-DD). Defaults to today when omitted."
|
|
2651
|
+
},
|
|
2652
|
+
content: {
|
|
2653
|
+
type: "string",
|
|
2654
|
+
description: "Text to append to the note body."
|
|
2655
|
+
},
|
|
2656
|
+
createIfMissing: {
|
|
2657
|
+
type: "boolean",
|
|
2658
|
+
description: "Create the note if it does not exist before appending. Default true."
|
|
2659
|
+
}
|
|
2660
|
+
},
|
|
2661
|
+
required: ["content"]
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
];
|
|
2665
|
+
function visibleTools(policy2) {
|
|
2666
|
+
if (!policy2.readOnly) return ALL_TOOLS;
|
|
2667
|
+
return ALL_TOOLS.filter((t) => !WRITE_TOOLS.has(t.name));
|
|
2668
|
+
}
|
|
2669
|
+
|
|
1995
2670
|
// src/watcher.ts
|
|
1996
2671
|
import { realpathSync } from "fs";
|
|
1997
2672
|
import { readFile as readFile10 } from "fs/promises";
|
|
1998
2673
|
import { join as join15, relative as relative2, sep as sep2 } from "path";
|
|
1999
2674
|
import chokidar from "chokidar";
|
|
2000
|
-
function removeNoteBacklinks(ctx2, relPath) {
|
|
2001
|
-
const oldDoc = ctx2.notes.get(relPath);
|
|
2002
|
-
if (oldDoc === void 0) return;
|
|
2003
|
-
for (const link of extractLinksWithLines(oldDoc.raw)) {
|
|
2004
|
-
const resolved = resolveLink(link.target, ctx2.notes);
|
|
2005
|
-
if (resolved === void 0) continue;
|
|
2006
|
-
const arr = ctx2.backlinks.get(resolved);
|
|
2007
|
-
if (arr === void 0) continue;
|
|
2008
|
-
const filtered = arr.filter((r) => r.path !== relPath);
|
|
2009
|
-
ctx2.backlinks.set(resolved, filtered);
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
function addNoteBacklinks(ctx2, relPath, raw) {
|
|
2013
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2014
|
-
for (const link of extractLinksWithLines(raw)) {
|
|
2015
|
-
const resolved = resolveLink(link.target, ctx2.notes);
|
|
2016
|
-
if (resolved === void 0) continue;
|
|
2017
|
-
const dedupeKey = `${relPath}\0${resolved}`;
|
|
2018
|
-
if (seen.has(dedupeKey)) continue;
|
|
2019
|
-
seen.add(dedupeKey);
|
|
2020
|
-
let arr = ctx2.backlinks.get(resolved);
|
|
2021
|
-
if (arr === void 0) {
|
|
2022
|
-
arr = [];
|
|
2023
|
-
ctx2.backlinks.set(resolved, arr);
|
|
2024
|
-
}
|
|
2025
|
-
const ref = { path: relPath, line: link.line, linkType: link.linkType };
|
|
2026
|
-
arr.push(ref);
|
|
2027
|
-
arr.sort((a, b) => a.path.localeCompare(b.path));
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
2675
|
function startWatcher(ctx2, log2, opts) {
|
|
2031
2676
|
const usePolling = opts?.usePolling ?? process.env.SEEKSTONE_WATCH_POLL === "1";
|
|
2032
2677
|
let vaultRoot2 = ctx2.vaultRoot;
|
|
@@ -2089,7 +2734,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
2089
2734
|
}
|
|
2090
2735
|
|
|
2091
2736
|
// src/index.ts
|
|
2092
|
-
var VERSION = true ? "0.
|
|
2737
|
+
var VERSION = true ? "0.10.0" : "0.0.0-dev";
|
|
2093
2738
|
var intent = parseCliIntent(process.argv.slice(2));
|
|
2094
2739
|
if (intent === "version") {
|
|
2095
2740
|
process.stdout.write(`${VERSION}
|
|
@@ -2101,6 +2746,11 @@ if (intent === "help") {
|
|
|
2101
2746
|
`);
|
|
2102
2747
|
process.exit(0);
|
|
2103
2748
|
}
|
|
2749
|
+
if (intent === "init-help") {
|
|
2750
|
+
process.stdout.write(`${initHelpText(VERSION)}
|
|
2751
|
+
`);
|
|
2752
|
+
process.exit(0);
|
|
2753
|
+
}
|
|
2104
2754
|
if (intent === "init") {
|
|
2105
2755
|
const result = await runInit(parseInitArgs(process.argv.slice(3)), {
|
|
2106
2756
|
env: process.env,
|
|
@@ -2119,400 +2769,18 @@ if (!vaultRoot) {
|
|
|
2119
2769
|
log.error("SEEKSTONE_VAULT env var is required");
|
|
2120
2770
|
process.exit(1);
|
|
2121
2771
|
}
|
|
2772
|
+
var policy = parseWritePolicy(process.env);
|
|
2773
|
+
if (policy.readOnly) log.info("read-only mode", { env: "SEEKSTONE_READ_ONLY" });
|
|
2774
|
+
if (policy.writeGlobs) log.info("write paths scoped", { globs: policy.writeGlobs });
|
|
2122
2775
|
log.info("building index", { vault: vaultRoot });
|
|
2123
2776
|
var { index, notes, backlinks, buildMs } = await buildIndex(vaultRoot);
|
|
2124
2777
|
log.info("index ready", { notes: notes.size, buildMs });
|
|
2125
|
-
var ctx = { vaultRoot, index, notes, backlinks };
|
|
2778
|
+
var ctx = { vaultRoot, index, notes, backlinks, policy };
|
|
2126
2779
|
var watcher = startWatcher(ctx, log);
|
|
2127
2780
|
process.on("exit", () => void watcher.stop());
|
|
2128
2781
|
var server = new Server({ name: "seekstone", version: VERSION }, { capabilities: { tools: {} } });
|
|
2129
2782
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2130
|
-
tools:
|
|
2131
|
-
{
|
|
2132
|
-
name: "search",
|
|
2133
|
-
description: "Full-text search across the vault. Returns ranked excerpts (~200 chars) \u2014 not full notes \u2014 to minimise context usage. Supports fuzzy matching and prefix search.",
|
|
2134
|
-
inputSchema: {
|
|
2135
|
-
type: "object",
|
|
2136
|
-
properties: {
|
|
2137
|
-
query: { type: "string", description: "Search query." },
|
|
2138
|
-
limit: { type: "number", description: "Max results (1\u201350, default 10)." },
|
|
2139
|
-
folder: { type: "string", description: "Restrict to a vault-relative folder prefix." },
|
|
2140
|
-
tag: { type: "string", description: "Restrict to notes with this tag." }
|
|
2141
|
-
},
|
|
2142
|
-
required: ["query"]
|
|
2143
|
-
}
|
|
2144
|
-
},
|
|
2145
|
-
{
|
|
2146
|
-
name: "query_notes",
|
|
2147
|
-
description: "Structured metadata query \u2014 filter notes by frontmatter key/value predicates, tag, folder, modified time, and size. Returns compact rows (path + title by default; opt into more via select), not note content. Use this instead of search when filtering by properties rather than text.",
|
|
2148
|
-
inputSchema: {
|
|
2149
|
-
type: "object",
|
|
2150
|
-
properties: {
|
|
2151
|
-
where: {
|
|
2152
|
-
type: "array",
|
|
2153
|
-
description: "Frontmatter predicates \u2014 all must match (AND).",
|
|
2154
|
-
items: {
|
|
2155
|
-
type: "object",
|
|
2156
|
-
properties: {
|
|
2157
|
-
key: { type: "string", description: "Frontmatter key to test." },
|
|
2158
|
-
op: {
|
|
2159
|
-
type: "string",
|
|
2160
|
-
enum: ["eq", "ne", "contains", "exists", "missing", "gt", "gte", "lt", "lte"],
|
|
2161
|
-
description: "eq/ne compare scalars; contains matches array membership or substring; exists/missing test key presence; gt/gte/lt/lte compare numbers or strings (ISO dates sort correctly)."
|
|
2162
|
-
},
|
|
2163
|
-
value: {
|
|
2164
|
-
description: "Comparison value. Required for every op except exists/missing."
|
|
2165
|
-
}
|
|
2166
|
-
},
|
|
2167
|
-
required: ["key", "op"]
|
|
2168
|
-
}
|
|
2169
|
-
},
|
|
2170
|
-
folder: { type: "string", description: "Restrict to a vault-relative folder prefix." },
|
|
2171
|
-
tag: { type: "string", description: "Restrict to notes with this tag (# optional)." },
|
|
2172
|
-
modifiedAfter: {
|
|
2173
|
-
type: "string",
|
|
2174
|
-
description: "Only notes modified at or after this ISO 8601 date/time."
|
|
2175
|
-
},
|
|
2176
|
-
modifiedBefore: {
|
|
2177
|
-
type: "string",
|
|
2178
|
-
description: "Only notes modified before this ISO 8601 date/time."
|
|
2179
|
-
},
|
|
2180
|
-
minSizeBytes: { type: "number", description: "Only notes at least this many bytes." },
|
|
2181
|
-
maxSizeBytes: { type: "number", description: "Only notes at most this many bytes." },
|
|
2182
|
-
select: {
|
|
2183
|
-
type: "array",
|
|
2184
|
-
items: { type: "string" },
|
|
2185
|
-
description: 'Extra fields per hit: frontmatter keys, or "mtime", "size", "tags". Default returns only path + title.'
|
|
2186
|
-
},
|
|
2187
|
-
sort: {
|
|
2188
|
-
type: "string",
|
|
2189
|
-
enum: ["path", "title", "mtime", "size"],
|
|
2190
|
-
description: "Sort field (default path)."
|
|
2191
|
-
},
|
|
2192
|
-
order: {
|
|
2193
|
-
type: "string",
|
|
2194
|
-
enum: ["asc", "desc"],
|
|
2195
|
-
description: "Sort order (default asc)."
|
|
2196
|
-
},
|
|
2197
|
-
limit: { type: "number", description: "Max results (1\u2013500, default 100)." }
|
|
2198
|
-
},
|
|
2199
|
-
required: []
|
|
2200
|
-
}
|
|
2201
|
-
},
|
|
2202
|
-
{
|
|
2203
|
-
name: "read_note",
|
|
2204
|
-
description: "Read a note or a span of it \u2014 by heading section, block reference, or line range. Returns structured JSON with the content, bytes returned, and total note size so payload savings are measurable. Use search or outline_note first to find the right path and section names.",
|
|
2205
|
-
inputSchema: {
|
|
2206
|
-
type: "object",
|
|
2207
|
-
properties: {
|
|
2208
|
-
path: {
|
|
2209
|
-
type: "string",
|
|
2210
|
-
description: 'Vault-relative path, e.g. "Daily Notes/2026-05-29.md".'
|
|
2211
|
-
},
|
|
2212
|
-
section: {
|
|
2213
|
-
type: "string",
|
|
2214
|
-
description: "Return only this heading section (heading text, # prefix optional). First match wins."
|
|
2215
|
-
},
|
|
2216
|
-
block: {
|
|
2217
|
-
type: "string",
|
|
2218
|
-
description: "Return only the line anchored by this block id (^ prefix optional)."
|
|
2219
|
-
},
|
|
2220
|
-
lines: {
|
|
2221
|
-
type: "object",
|
|
2222
|
-
description: "Return only this line range (1-indexed, inclusive).",
|
|
2223
|
-
properties: {
|
|
2224
|
-
from: { type: "number", description: "First line (1-indexed)." },
|
|
2225
|
-
to: {
|
|
2226
|
-
type: "number",
|
|
2227
|
-
description: "Last line (1-indexed, inclusive). Defaults to EOF."
|
|
2228
|
-
}
|
|
2229
|
-
},
|
|
2230
|
-
required: ["from"]
|
|
2231
|
-
},
|
|
2232
|
-
includeFrontmatter: {
|
|
2233
|
-
type: "boolean",
|
|
2234
|
-
description: "Prepend frontmatter to section/block span results. Default false for spans."
|
|
2235
|
-
}
|
|
2236
|
-
},
|
|
2237
|
-
required: ["path"]
|
|
2238
|
-
}
|
|
2239
|
-
},
|
|
2240
|
-
{
|
|
2241
|
-
name: "list_notes",
|
|
2242
|
-
description: "List notes, optionally filtered by folder prefix or tag.",
|
|
2243
|
-
inputSchema: {
|
|
2244
|
-
type: "object",
|
|
2245
|
-
properties: {
|
|
2246
|
-
folder: { type: "string", description: "Vault-relative folder prefix." },
|
|
2247
|
-
tag: { type: "string", description: "Filter by tag (# prefix optional)." },
|
|
2248
|
-
limit: { type: "number", description: "Max results (1\u2013500, default 100)." }
|
|
2249
|
-
},
|
|
2250
|
-
required: []
|
|
2251
|
-
}
|
|
2252
|
-
},
|
|
2253
|
-
{
|
|
2254
|
-
name: "list_tags",
|
|
2255
|
-
description: "List all tags in the vault with usage counts. Supports substring filtering, minimum count threshold, and sort order. Nested tags (e.g. area/work) include a parent field.",
|
|
2256
|
-
inputSchema: {
|
|
2257
|
-
type: "object",
|
|
2258
|
-
properties: {
|
|
2259
|
-
pattern: {
|
|
2260
|
-
type: "string",
|
|
2261
|
-
description: 'Substring filter on tag name. Example: "work" matches "work" and "area/work".'
|
|
2262
|
-
},
|
|
2263
|
-
minCount: {
|
|
2264
|
-
type: "number",
|
|
2265
|
-
description: "Only return tags used in at least this many notes."
|
|
2266
|
-
},
|
|
2267
|
-
sort: {
|
|
2268
|
-
type: "string",
|
|
2269
|
-
enum: ["count", "name"],
|
|
2270
|
-
description: "Sort by usage count descending (default) or alphabetically."
|
|
2271
|
-
}
|
|
2272
|
-
},
|
|
2273
|
-
required: []
|
|
2274
|
-
}
|
|
2275
|
-
},
|
|
2276
|
-
{
|
|
2277
|
-
name: "create_note",
|
|
2278
|
-
description: "Create a new note at a vault-relative path. Optionally sets frontmatter and body content. Parent directories are created automatically. Fails if the note already exists unless overwrite is true.",
|
|
2279
|
-
inputSchema: {
|
|
2280
|
-
type: "object",
|
|
2281
|
-
properties: {
|
|
2282
|
-
path: {
|
|
2283
|
-
type: "string",
|
|
2284
|
-
description: 'Vault-relative path, e.g. "Daily Notes/2026-06-01.md".'
|
|
2285
|
-
},
|
|
2286
|
-
content: { type: "string", description: "Body content for the note." },
|
|
2287
|
-
frontmatter: {
|
|
2288
|
-
type: "object",
|
|
2289
|
-
description: "Frontmatter key-value pairs.",
|
|
2290
|
-
additionalProperties: true
|
|
2291
|
-
},
|
|
2292
|
-
overwrite: {
|
|
2293
|
-
type: "boolean",
|
|
2294
|
-
description: "Overwrite an existing note. Defaults to false."
|
|
2295
|
-
}
|
|
2296
|
-
},
|
|
2297
|
-
required: ["path"]
|
|
2298
|
-
}
|
|
2299
|
-
},
|
|
2300
|
-
{
|
|
2301
|
-
name: "delete_note",
|
|
2302
|
-
description: "Permanently delete a note from the vault. This cannot be undone.",
|
|
2303
|
-
inputSchema: {
|
|
2304
|
-
type: "object",
|
|
2305
|
-
properties: {
|
|
2306
|
-
path: { type: "string", description: "Vault-relative path of the note to delete." }
|
|
2307
|
-
},
|
|
2308
|
-
required: ["path"]
|
|
2309
|
-
}
|
|
2310
|
-
},
|
|
2311
|
-
{
|
|
2312
|
-
name: "move_note",
|
|
2313
|
-
description: "Move or rename a note to a new vault-relative path. Parent directories at the destination are created automatically. Fails if the destination already exists unless overwrite is true.",
|
|
2314
|
-
inputSchema: {
|
|
2315
|
-
type: "object",
|
|
2316
|
-
properties: {
|
|
2317
|
-
from: { type: "string", description: "Vault-relative source path." },
|
|
2318
|
-
to: { type: "string", description: "Vault-relative destination path." },
|
|
2319
|
-
overwrite: {
|
|
2320
|
-
type: "boolean",
|
|
2321
|
-
description: "Overwrite destination if it exists. Defaults to false."
|
|
2322
|
-
}
|
|
2323
|
-
},
|
|
2324
|
-
required: ["from", "to"]
|
|
2325
|
-
}
|
|
2326
|
-
},
|
|
2327
|
-
{
|
|
2328
|
-
name: "append_note",
|
|
2329
|
-
description: "Append text to a note body without touching the frontmatter. Safe for meeting notes, daily logs, and append-only workflows.",
|
|
2330
|
-
inputSchema: {
|
|
2331
|
-
type: "object",
|
|
2332
|
-
properties: {
|
|
2333
|
-
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2334
|
-
content: { type: "string", description: "Text to append." }
|
|
2335
|
-
},
|
|
2336
|
-
required: ["path", "content"]
|
|
2337
|
-
}
|
|
2338
|
-
},
|
|
2339
|
-
{
|
|
2340
|
-
name: "patch_frontmatter",
|
|
2341
|
-
description: "Set, update, or delete frontmatter keys without reordering existing keys or changing quote style. Pass null as a value to delete a key.",
|
|
2342
|
-
inputSchema: {
|
|
2343
|
-
type: "object",
|
|
2344
|
-
properties: {
|
|
2345
|
-
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2346
|
-
patch: {
|
|
2347
|
-
type: "object",
|
|
2348
|
-
description: "Key-value pairs to set. Null value removes the key.",
|
|
2349
|
-
additionalProperties: true
|
|
2350
|
-
}
|
|
2351
|
-
},
|
|
2352
|
-
required: ["path", "patch"]
|
|
2353
|
-
}
|
|
2354
|
-
},
|
|
2355
|
-
{
|
|
2356
|
-
name: "outline_note",
|
|
2357
|
-
description: "Return a note's structure \u2014 heading tree with offsets, block-reference anchors, and frontmatter key list \u2014 without returning any prose. Use this before section reads or patches to discover what sections exist at a fraction of the cost of reading the full note.",
|
|
2358
|
-
inputSchema: {
|
|
2359
|
-
type: "object",
|
|
2360
|
-
properties: {
|
|
2361
|
-
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2362
|
-
includeBlocks: {
|
|
2363
|
-
type: "boolean",
|
|
2364
|
-
description: "Include block-reference anchors (^id). Default true."
|
|
2365
|
-
},
|
|
2366
|
-
includeSizes: {
|
|
2367
|
-
type: "boolean",
|
|
2368
|
-
description: "Include per-section character length in each heading entry. Default false."
|
|
2369
|
-
}
|
|
2370
|
-
},
|
|
2371
|
-
required: ["path"]
|
|
2372
|
-
}
|
|
2373
|
-
},
|
|
2374
|
-
{
|
|
2375
|
-
name: "patch_note",
|
|
2376
|
-
description: "Surgically edit a section of a note \u2014 targeted by heading or block reference \u2014 without rewriting the whole file. Operations: append (add after section), prepend (add after heading line), replace (swap section content). Frontmatter is never touched.",
|
|
2377
|
-
inputSchema: {
|
|
2378
|
-
type: "object",
|
|
2379
|
-
properties: {
|
|
2380
|
-
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2381
|
-
target: {
|
|
2382
|
-
type: "object",
|
|
2383
|
-
description: 'Exactly one of: { heading: "Section Title" } or { block: "block-id" } (without the ^ prefix).'
|
|
2384
|
-
},
|
|
2385
|
-
operation: {
|
|
2386
|
-
type: "string",
|
|
2387
|
-
enum: ["append", "prepend", "replace"],
|
|
2388
|
-
description: "append: insert after section content. prepend: insert after heading line. replace: swap section content."
|
|
2389
|
-
},
|
|
2390
|
-
content: { type: "string", description: "Content to insert or replace with." },
|
|
2391
|
-
createIfMissing: {
|
|
2392
|
-
type: "boolean",
|
|
2393
|
-
description: "If the heading target is not found, append a new heading (level 2) + content. Only valid for heading targets. Default false."
|
|
2394
|
-
}
|
|
2395
|
-
},
|
|
2396
|
-
required: ["path", "target", "operation", "content"]
|
|
2397
|
-
}
|
|
2398
|
-
},
|
|
2399
|
-
{
|
|
2400
|
-
name: "get_backlinks",
|
|
2401
|
-
description: "Return every note that links to the target note, with the source line and an optional excerpt. Results come from the pre-built reverse-link index so this is a fast, pure index lookup. Sort order: source path ascending.",
|
|
2402
|
-
inputSchema: {
|
|
2403
|
-
type: "object",
|
|
2404
|
-
properties: {
|
|
2405
|
-
path: { type: "string", description: "Vault-relative path to the target note." },
|
|
2406
|
-
includeContext: {
|
|
2407
|
-
type: "boolean",
|
|
2408
|
-
description: "Include a short excerpt (~200 chars) from the linking line. Default true."
|
|
2409
|
-
},
|
|
2410
|
-
limit: {
|
|
2411
|
-
type: "number",
|
|
2412
|
-
description: "Max backlinks to return (1\u2013500). Default 50."
|
|
2413
|
-
}
|
|
2414
|
-
},
|
|
2415
|
-
required: ["path"]
|
|
2416
|
-
}
|
|
2417
|
-
},
|
|
2418
|
-
{
|
|
2419
|
-
name: "get_links",
|
|
2420
|
-
description: "Return all outgoing wikilinks and embeds from a note. Each link is marked resolved (with target path) or unresolved. Duplicate targets are de-duplicated; results sorted by line number.",
|
|
2421
|
-
inputSchema: {
|
|
2422
|
-
type: "object",
|
|
2423
|
-
properties: {
|
|
2424
|
-
path: { type: "string", description: "Vault-relative path to the note." }
|
|
2425
|
-
},
|
|
2426
|
-
required: ["path"]
|
|
2427
|
-
}
|
|
2428
|
-
},
|
|
2429
|
-
{
|
|
2430
|
-
name: "replace_in_note",
|
|
2431
|
-
description: "Find and replace text within a note body. Supports literal and regex search, case sensitivity, whole-word matching, and a replacement limit. Frontmatter is never touched. Use dryRun to preview matches before writing.",
|
|
2432
|
-
inputSchema: {
|
|
2433
|
-
type: "object",
|
|
2434
|
-
properties: {
|
|
2435
|
-
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2436
|
-
find: { type: "string", description: "Text or pattern to find." },
|
|
2437
|
-
replace: {
|
|
2438
|
-
type: "string",
|
|
2439
|
-
description: "Replacement text. Supports $1, $2, \u2026 backreferences in regex mode."
|
|
2440
|
-
},
|
|
2441
|
-
regex: {
|
|
2442
|
-
type: "boolean",
|
|
2443
|
-
description: "Treat find as a regular expression. Default false."
|
|
2444
|
-
},
|
|
2445
|
-
caseSensitive: {
|
|
2446
|
-
type: "boolean",
|
|
2447
|
-
description: "Case-sensitive matching. Default false."
|
|
2448
|
-
},
|
|
2449
|
-
wholeWord: {
|
|
2450
|
-
type: "boolean",
|
|
2451
|
-
description: "Match whole words only (\\b boundary). Default false."
|
|
2452
|
-
},
|
|
2453
|
-
limit: {
|
|
2454
|
-
type: "number",
|
|
2455
|
-
description: "Maximum number of replacements. Omit to replace all."
|
|
2456
|
-
},
|
|
2457
|
-
dryRun: {
|
|
2458
|
-
type: "boolean",
|
|
2459
|
-
description: "If true, report matches without writing. Default false."
|
|
2460
|
-
}
|
|
2461
|
-
},
|
|
2462
|
-
required: ["path", "find", "replace"]
|
|
2463
|
-
}
|
|
2464
|
-
},
|
|
2465
|
-
{
|
|
2466
|
-
name: "get_periodic_note",
|
|
2467
|
-
description: "Get the path and existence status of a periodic note (daily, weekly, monthly, quarterly, or yearly) for a given date. Reads folder/format config from .obsidian/daily-notes.json (daily) or the periodic-notes plugin data.json. Optionally creates the note from the configured template if it is missing.",
|
|
2468
|
-
inputSchema: {
|
|
2469
|
-
type: "object",
|
|
2470
|
-
properties: {
|
|
2471
|
-
period: {
|
|
2472
|
-
type: "string",
|
|
2473
|
-
enum: ["daily", "weekly", "monthly", "quarterly", "yearly"],
|
|
2474
|
-
description: "Period type. Default: daily."
|
|
2475
|
-
},
|
|
2476
|
-
date: {
|
|
2477
|
-
type: "string",
|
|
2478
|
-
description: "ISO date string (YYYY-MM-DD). Defaults to today when omitted."
|
|
2479
|
-
},
|
|
2480
|
-
createIfMissing: {
|
|
2481
|
-
type: "boolean",
|
|
2482
|
-
description: "Create the note from the configured template if it does not exist. Default false."
|
|
2483
|
-
}
|
|
2484
|
-
},
|
|
2485
|
-
required: []
|
|
2486
|
-
}
|
|
2487
|
-
},
|
|
2488
|
-
{
|
|
2489
|
-
name: "append_periodic_note",
|
|
2490
|
-
description: "Append text to a periodic note (daily, weekly, monthly, quarterly, or yearly). Preserves existing frontmatter exactly. Creates the note first (from template if configured) when createIfMissing is true (default).",
|
|
2491
|
-
inputSchema: {
|
|
2492
|
-
type: "object",
|
|
2493
|
-
properties: {
|
|
2494
|
-
period: {
|
|
2495
|
-
type: "string",
|
|
2496
|
-
enum: ["daily", "weekly", "monthly", "quarterly", "yearly"],
|
|
2497
|
-
description: "Period type. Default: daily."
|
|
2498
|
-
},
|
|
2499
|
-
date: {
|
|
2500
|
-
type: "string",
|
|
2501
|
-
description: "ISO date string (YYYY-MM-DD). Defaults to today when omitted."
|
|
2502
|
-
},
|
|
2503
|
-
content: {
|
|
2504
|
-
type: "string",
|
|
2505
|
-
description: "Text to append to the note body."
|
|
2506
|
-
},
|
|
2507
|
-
createIfMissing: {
|
|
2508
|
-
type: "boolean",
|
|
2509
|
-
description: "Create the note if it does not exist before appending. Default true."
|
|
2510
|
-
}
|
|
2511
|
-
},
|
|
2512
|
-
required: ["content"]
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
]
|
|
2783
|
+
tools: visibleTools(ctx.policy)
|
|
2516
2784
|
}));
|
|
2517
2785
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
2518
2786
|
const { name, arguments: args } = req.params;
|
|
@@ -2520,7 +2788,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2520
2788
|
});
|
|
2521
2789
|
var transport = new StdioServerTransport();
|
|
2522
2790
|
await server.connect(transport);
|
|
2523
|
-
log.info("ready", { tools:
|
|
2791
|
+
log.info("ready", { tools: visibleTools(ctx.policy).length, transport: "stdio" });
|
|
2524
2792
|
process.stderr.write(
|
|
2525
2793
|
`seekstone: add to Claude Desktop:
|
|
2526
2794
|
${JSON.stringify(
|