seekstone 0.12.2 → 0.13.1
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 +3 -2
- package/dist/index.js +291 -50
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ Other MCP clients (Windsurf, Cline, …) take the Option 3 JSON block in their o
|
|
|
104
104
|
|
|
105
105
|
## Tools
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
19 tools: 10 read, 9 write.
|
|
108
108
|
|
|
109
109
|
### Read
|
|
110
110
|
|
|
@@ -128,6 +128,7 @@ Other MCP clients (Windsurf, Cline, …) take the Option 3 JSON block in their o
|
|
|
128
128
|
| `create_note` | Create a note (optional frontmatter + body); parent dirs created automatically. Never clobbers an existing note unless you pass `overwrite: true`. |
|
|
129
129
|
| `delete_note` | Move a note to the vault's `.trash/` folder (Obsidian-compatible, restorable). Pass `permanent: true` for an unrecoverable delete. |
|
|
130
130
|
| `move_note` | Move/rename a note — wikilinks and markdown links in other notes that point at it are rewritten so nothing breaks; destination dirs created automatically. |
|
|
131
|
+
| `rename_heading` | Rename a heading in a note — every `[[note#heading]]` wikilink and embed across the vault is rewritten so references keep working (aliases preserved, fenced code blocks left alone). |
|
|
131
132
|
| `append_note` | Append to a note body without touching frontmatter. |
|
|
132
133
|
| `patch_frontmatter` | Set/update/delete frontmatter keys without reordering existing keys or changing quote style. |
|
|
133
134
|
| `patch_note` | Insert text immediately after a heading without touching frontmatter. |
|
|
@@ -147,7 +148,7 @@ The content-editing tools (`append_note`, `patch_note`, `patch_frontmatter`, `re
|
|
|
147
148
|
| `SEEKSTONE_LOG_FILE` | no | Absolute path; when set, JSON-line logs are appended here (size-rotated). |
|
|
148
149
|
| `SEEKSTONE_WATCH_POLL` | no | Set to `1` to stat-poll for changes instead of native OS events — reliable on network drives, WSL, containers. |
|
|
149
150
|
| `SEEKSTONE_LOG_MAX_SIZE` | no | Log-rotation threshold for `SEEKSTONE_LOG_FILE` (e.g. `10mb`; default 5 MB). |
|
|
150
|
-
| `SEEKSTONE_READ_ONLY` | no | Set to `1` to run read-only: the
|
|
151
|
+
| `SEEKSTONE_READ_ONLY` | no | Set to `1` to run read-only: the 9 write tools are unregistered entirely (and rejected if called anyway), so the session provably cannot modify your vault. |
|
|
151
152
|
| `SEEKSTONE_WRITE_PATHS` | no | Comma-separated vault-relative globs (e.g. `journal/**,inbox/*.md`). Writes are permitted only under matching paths; the rest of the vault stays read-only. |
|
|
152
153
|
|
|
153
154
|
---
|
package/dist/index.js
CHANGED
|
@@ -246,25 +246,64 @@ async function appendNote(ctx2, input) {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
// ../core/src/extract.ts
|
|
249
|
+
import { parseDocument, visit } from "yaml";
|
|
249
250
|
var LINK_WITH_LINES_RE = /(!?\[\[)([^\]|#\n]{1,512})(#[^\]|\n]{1,512})?(\|[^\]\n]{1,512})?(\]\])/g;
|
|
250
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) {
|
|
251
261
|
const out = [];
|
|
252
|
-
|
|
262
|
+
linksFromLinesInto(out, text, firstLine);
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
function linksFromLinesInto(out, text, firstLine) {
|
|
266
|
+
const lines = text.split("\n");
|
|
253
267
|
for (const [lineIdx, lineText] of lines.entries()) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
});
|
|
265
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
|
+
});
|
|
266
300
|
return out;
|
|
267
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
|
+
}
|
|
268
307
|
var INLINE_TAG_RE = /(?:^|[^\w\]/])#([A-Za-z_][\w/-]*)/g;
|
|
269
308
|
function extractInlineTags(body) {
|
|
270
309
|
const out = [];
|
|
@@ -873,6 +912,16 @@ import { posix } from "path";
|
|
|
873
912
|
var WIKI_RE = /(!?)\[\[([^\]|#\n]{1,512})(#[^\]|\n]{1,512})?(\|[^\]\n]{1,512})?\]\]/g;
|
|
874
913
|
var MD_RE = /(!?)\[([^\]\n]*)\]\(([^)\n]+)\)/g;
|
|
875
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
|
+
}
|
|
876
925
|
function wikiTargetFor(newPath, postNotes) {
|
|
877
926
|
const noExt = newPath.replace(/\.md$/i, "");
|
|
878
927
|
const base = noExt.split("/").pop() ?? noExt;
|
|
@@ -932,16 +981,8 @@ function rewriteNoteLinks(raw, sourcePath, oldPath, newPath, preNotes, postNotes
|
|
|
932
981
|
const target = needsWrap ? `<${next}${fragment}>` : `${next}${fragment}`;
|
|
933
982
|
return `${bang}[${text}](${target})`;
|
|
934
983
|
});
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (FENCE_RE.test(line)) {
|
|
938
|
-
inFence = !inFence;
|
|
939
|
-
return line;
|
|
940
|
-
}
|
|
941
|
-
if (inFence) return line;
|
|
942
|
-
return rewriteMdLine(rewriteWikiLine(line));
|
|
943
|
-
});
|
|
944
|
-
return { content: lines.join("\n"), count };
|
|
984
|
+
const content = mapUnfencedLines(raw, (line) => rewriteMdLine(rewriteWikiLine(line)));
|
|
985
|
+
return { content, count };
|
|
945
986
|
}
|
|
946
987
|
function hasMarkdownLinkTo(raw, sourcePath, targetPath) {
|
|
947
988
|
if (!raw.includes("](")) return false;
|
|
@@ -1111,7 +1152,7 @@ async function outlineNote(ctx2, input) {
|
|
|
1111
1152
|
|
|
1112
1153
|
// src/tools/patch_frontmatter.ts
|
|
1113
1154
|
import { readFile as readFile4 } from "fs/promises";
|
|
1114
|
-
import { parseDocument } from "yaml";
|
|
1155
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
1115
1156
|
import { z as z12 } from "zod";
|
|
1116
1157
|
var PatchFrontmatterInput = z12.object({
|
|
1117
1158
|
path: z12.string().describe("Vault-relative path to the note."),
|
|
@@ -1133,7 +1174,7 @@ async function patchFrontmatter(ctx2, input) {
|
|
|
1133
1174
|
const keysRemoved = [];
|
|
1134
1175
|
let newContent;
|
|
1135
1176
|
if (!fm.present) {
|
|
1136
|
-
const doc =
|
|
1177
|
+
const doc = parseDocument2("");
|
|
1137
1178
|
for (const [k, v] of Object.entries(input.patch)) {
|
|
1138
1179
|
if (v !== null) {
|
|
1139
1180
|
doc.set(k, v);
|
|
@@ -1148,7 +1189,7 @@ ${original}`;
|
|
|
1148
1189
|
const openLen = opensWithCRLF ? 5 : 4;
|
|
1149
1190
|
const closeLen = opensWithCRLF ? 7 : 5;
|
|
1150
1191
|
const yamlText = original.slice(openLen, fm.bodyStart - closeLen);
|
|
1151
|
-
const doc =
|
|
1192
|
+
const doc = parseDocument2(yamlText, { keepSourceTokens: true });
|
|
1152
1193
|
for (const [k, v] of Object.entries(input.patch)) {
|
|
1153
1194
|
const existing = doc.has(k);
|
|
1154
1195
|
if (v === null) {
|
|
@@ -1735,19 +1776,142 @@ async function readNote(ctx2, input) {
|
|
|
1735
1776
|
};
|
|
1736
1777
|
}
|
|
1737
1778
|
|
|
1738
|
-
// src/tools/
|
|
1779
|
+
// src/tools/rename_heading.ts
|
|
1739
1780
|
import { readFile as readFile8 } from "fs/promises";
|
|
1781
|
+
import { join as join4 } from "path";
|
|
1740
1782
|
import { z as z17 } from "zod";
|
|
1741
|
-
var
|
|
1742
|
-
path: z17.string().describe("Vault-relative path
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
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."),
|
|
1750
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(
|
|
1751
1915
|
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
1752
1916
|
)
|
|
1753
1917
|
});
|
|
@@ -1773,7 +1937,7 @@ async function replaceInNote(ctx2, input) {
|
|
|
1773
1937
|
} catch (err) {
|
|
1774
1938
|
throw new Error(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
1775
1939
|
}
|
|
1776
|
-
const raw = await
|
|
1940
|
+
const raw = await readFile9(absPath, "utf8");
|
|
1777
1941
|
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
|
|
1778
1942
|
const fm = parseFrontmatter(raw);
|
|
1779
1943
|
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
@@ -1805,7 +1969,7 @@ async function replaceInNote(ctx2, input) {
|
|
|
1805
1969
|
newBody += body.slice(lastIndex);
|
|
1806
1970
|
const newContent = `${originalFmRegion}${newBody}`;
|
|
1807
1971
|
await atomicWrite(absPath, newContent);
|
|
1808
|
-
const written = await
|
|
1972
|
+
const written = await readFile9(absPath, "utf8");
|
|
1809
1973
|
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
1810
1974
|
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
1811
1975
|
}
|
|
@@ -1830,6 +1994,7 @@ var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1830
1994
|
"create_note",
|
|
1831
1995
|
"delete_note",
|
|
1832
1996
|
"move_note",
|
|
1997
|
+
"rename_heading",
|
|
1833
1998
|
"append_note",
|
|
1834
1999
|
"patch_frontmatter",
|
|
1835
2000
|
"patch_note",
|
|
@@ -1973,6 +2138,15 @@ async function run(ctx2, name, args) {
|
|
|
1973
2138
|
}
|
|
1974
2139
|
return { content: [{ type: "text", text }] };
|
|
1975
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
|
+
}
|
|
1976
2150
|
case "append_note": {
|
|
1977
2151
|
const input = AppendNoteInput.parse(args);
|
|
1978
2152
|
const result = await appendNote(ctx2, input);
|
|
@@ -2038,12 +2212,12 @@ async function run(ctx2, name, args) {
|
|
|
2038
2212
|
}
|
|
2039
2213
|
|
|
2040
2214
|
// src/index/build.ts
|
|
2041
|
-
import { readFile as
|
|
2215
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2042
2216
|
import { basename as basename3, extname as extname4 } from "path";
|
|
2043
2217
|
|
|
2044
2218
|
// ../core/src/walk.ts
|
|
2045
2219
|
import { stat } from "fs/promises";
|
|
2046
|
-
import { extname as extname3, join as
|
|
2220
|
+
import { extname as extname3, join as join5, relative, sep as sep2 } from "path";
|
|
2047
2221
|
import fg from "fast-glob";
|
|
2048
2222
|
var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".heic"]);
|
|
2049
2223
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
|
|
@@ -2120,7 +2294,7 @@ async function buildIndex(vaultRoot2) {
|
|
|
2120
2294
|
await Promise.all(
|
|
2121
2295
|
noteEntries.map(async (entry) => {
|
|
2122
2296
|
try {
|
|
2123
|
-
const raw = await
|
|
2297
|
+
const raw = await readFile10(entry.absPath, "utf8");
|
|
2124
2298
|
const fm = parseFrontmatter(raw);
|
|
2125
2299
|
const inlineTags = extractInlineTags(fm.body);
|
|
2126
2300
|
const fmTags = frontmatterTags(fm.data);
|
|
@@ -2173,8 +2347,8 @@ function buildBacklinks(notes2) {
|
|
|
2173
2347
|
|
|
2174
2348
|
// src/init.ts
|
|
2175
2349
|
import { execFileSync } from "child_process";
|
|
2176
|
-
import { access as access5, copyFile, mkdir as mkdir5, readdir, readFile as
|
|
2177
|
-
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";
|
|
2178
2352
|
function parseInitArgs(argv) {
|
|
2179
2353
|
const opts = { write: false, client: "desktop" };
|
|
2180
2354
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -2217,7 +2391,7 @@ function cursorConfigPath(platform, env) {
|
|
|
2217
2391
|
return j(env.home ?? "", ".cursor", "mcp.json");
|
|
2218
2392
|
}
|
|
2219
2393
|
function vscodeMcpConfigPath(cwd) {
|
|
2220
|
-
return
|
|
2394
|
+
return join6(cwd, ".vscode", "mcp.json");
|
|
2221
2395
|
}
|
|
2222
2396
|
function obsidianRegistryPath(platform, env) {
|
|
2223
2397
|
if (platform === "win32") {
|
|
@@ -2279,7 +2453,7 @@ async function validateVault(vaultPath) {
|
|
|
2279
2453
|
return { ok: false, error: `Path does not exist: ${vaultPath}` };
|
|
2280
2454
|
}
|
|
2281
2455
|
try {
|
|
2282
|
-
await access5(
|
|
2456
|
+
await access5(join6(vaultPath, ".obsidian"));
|
|
2283
2457
|
} catch {
|
|
2284
2458
|
return {
|
|
2285
2459
|
ok: false,
|
|
@@ -2299,7 +2473,7 @@ async function countMarkdown(root) {
|
|
|
2299
2473
|
}
|
|
2300
2474
|
for (const e of entries) {
|
|
2301
2475
|
if (e.name.startsWith(".")) continue;
|
|
2302
|
-
const full =
|
|
2476
|
+
const full = join6(dir, e.name);
|
|
2303
2477
|
if (e.isDirectory()) await walk(full);
|
|
2304
2478
|
else if (e.name.endsWith(".md")) count++;
|
|
2305
2479
|
}
|
|
@@ -2317,7 +2491,7 @@ async function runInit(opts, deps) {
|
|
|
2317
2491
|
]
|
|
2318
2492
|
};
|
|
2319
2493
|
}
|
|
2320
|
-
const rf = deps.readFile ??
|
|
2494
|
+
const rf = deps.readFile ?? readFile11;
|
|
2321
2495
|
let vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT;
|
|
2322
2496
|
if (!vaultPath) {
|
|
2323
2497
|
const registryPath = obsidianRegistryPath(deps.platform, {
|
|
@@ -2578,9 +2752,32 @@ function installProcessGuards(log2) {
|
|
|
2578
2752
|
}
|
|
2579
2753
|
|
|
2580
2754
|
// src/tool-list.ts
|
|
2755
|
+
var READ_ONLY_ANNOTATIONS = {
|
|
2756
|
+
readOnlyHint: true,
|
|
2757
|
+
openWorldHint: false
|
|
2758
|
+
};
|
|
2759
|
+
var ADDITIVE_WRITE_ANNOTATIONS = {
|
|
2760
|
+
readOnlyHint: false,
|
|
2761
|
+
destructiveHint: false,
|
|
2762
|
+
idempotentHint: false,
|
|
2763
|
+
openWorldHint: false
|
|
2764
|
+
};
|
|
2765
|
+
var DESTRUCTIVE_WRITE_ANNOTATIONS = {
|
|
2766
|
+
readOnlyHint: false,
|
|
2767
|
+
destructiveHint: true,
|
|
2768
|
+
idempotentHint: false,
|
|
2769
|
+
openWorldHint: false
|
|
2770
|
+
};
|
|
2771
|
+
var IDEMPOTENT_DESTRUCTIVE_WRITE_ANNOTATIONS = {
|
|
2772
|
+
readOnlyHint: false,
|
|
2773
|
+
destructiveHint: true,
|
|
2774
|
+
idempotentHint: true,
|
|
2775
|
+
openWorldHint: false
|
|
2776
|
+
};
|
|
2581
2777
|
var ALL_TOOLS = [
|
|
2582
2778
|
{
|
|
2583
2779
|
name: "search",
|
|
2780
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
2584
2781
|
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.",
|
|
2585
2782
|
inputSchema: {
|
|
2586
2783
|
type: "object",
|
|
@@ -2595,6 +2792,7 @@ var ALL_TOOLS = [
|
|
|
2595
2792
|
},
|
|
2596
2793
|
{
|
|
2597
2794
|
name: "query_notes",
|
|
2795
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
2598
2796
|
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.",
|
|
2599
2797
|
inputSchema: {
|
|
2600
2798
|
type: "object",
|
|
@@ -2652,6 +2850,7 @@ var ALL_TOOLS = [
|
|
|
2652
2850
|
},
|
|
2653
2851
|
{
|
|
2654
2852
|
name: "context_pack",
|
|
2853
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
2655
2854
|
description: 'Assemble everything needed to ANSWER a natural-language question in one call, under a strict byte budget (default 2048): ranked excerpts, linked neighbor notes (backlinks/outlinks) with one-line summaries, and follow-up source paths. Use search to locate notes and query_notes for metadata filters; use context_pack when you want answer-ready context without multiple round-trips. Empty excerpts with confidence "none" or "low" means the vault lacks coverage \u2014 do not infer content.',
|
|
2656
2855
|
inputSchema: {
|
|
2657
2856
|
type: "object",
|
|
@@ -2667,6 +2866,7 @@ var ALL_TOOLS = [
|
|
|
2667
2866
|
},
|
|
2668
2867
|
{
|
|
2669
2868
|
name: "read_note",
|
|
2869
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
2670
2870
|
description: "Read a note or a span of it \u2014 by heading section, block reference, or line range. Returns structured JSON with the content, bytes returned, total note size, and a contentHash to pass as prevHash to edit tools for compare-and-swap. Use search or outline_note first to find the right path and section names.",
|
|
2671
2871
|
inputSchema: {
|
|
2672
2872
|
type: "object",
|
|
@@ -2705,6 +2905,7 @@ var ALL_TOOLS = [
|
|
|
2705
2905
|
},
|
|
2706
2906
|
{
|
|
2707
2907
|
name: "list_notes",
|
|
2908
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
2708
2909
|
description: "List notes, optionally filtered by folder prefix or tag.",
|
|
2709
2910
|
inputSchema: {
|
|
2710
2911
|
type: "object",
|
|
@@ -2718,6 +2919,7 @@ var ALL_TOOLS = [
|
|
|
2718
2919
|
},
|
|
2719
2920
|
{
|
|
2720
2921
|
name: "list_tags",
|
|
2922
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
2721
2923
|
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.",
|
|
2722
2924
|
inputSchema: {
|
|
2723
2925
|
type: "object",
|
|
@@ -2741,6 +2943,7 @@ var ALL_TOOLS = [
|
|
|
2741
2943
|
},
|
|
2742
2944
|
{
|
|
2743
2945
|
name: "create_note",
|
|
2946
|
+
annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
|
|
2744
2947
|
description: "Create a new note at a vault-relative path. Optionally sets frontmatter and body content. Parent directories are created automatically. Fails if the note already exists unless overwrite is true (prevHash may guard the overwrite).",
|
|
2745
2948
|
inputSchema: {
|
|
2746
2949
|
type: "object",
|
|
@@ -2769,6 +2972,7 @@ var ALL_TOOLS = [
|
|
|
2769
2972
|
},
|
|
2770
2973
|
{
|
|
2771
2974
|
name: "delete_note",
|
|
2975
|
+
annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
|
|
2772
2976
|
description: "Delete a note. By default it is moved to the vault .trash/ folder (recoverable by moving it back); pass permanent: true to remove it outright.",
|
|
2773
2977
|
inputSchema: {
|
|
2774
2978
|
type: "object",
|
|
@@ -2784,6 +2988,7 @@ var ALL_TOOLS = [
|
|
|
2784
2988
|
},
|
|
2785
2989
|
{
|
|
2786
2990
|
name: "move_note",
|
|
2991
|
+
annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
|
|
2787
2992
|
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.",
|
|
2788
2993
|
inputSchema: {
|
|
2789
2994
|
type: "object",
|
|
@@ -2802,8 +3007,36 @@ var ALL_TOOLS = [
|
|
|
2802
3007
|
required: ["from", "to"]
|
|
2803
3008
|
}
|
|
2804
3009
|
},
|
|
3010
|
+
{
|
|
3011
|
+
name: "rename_heading",
|
|
3012
|
+
annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
|
|
3013
|
+
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.",
|
|
3014
|
+
inputSchema: {
|
|
3015
|
+
type: "object",
|
|
3016
|
+
properties: {
|
|
3017
|
+
path: {
|
|
3018
|
+
type: "string",
|
|
3019
|
+
description: "Vault-relative path of the note containing the heading."
|
|
3020
|
+
},
|
|
3021
|
+
oldHeading: {
|
|
3022
|
+
type: "string",
|
|
3023
|
+
description: "Current heading text, without # markers. Case-insensitive; first match wins."
|
|
3024
|
+
},
|
|
3025
|
+
newHeading: {
|
|
3026
|
+
type: "string",
|
|
3027
|
+
description: 'New heading text, without # markers. Must not contain newlines, "[", "]", "|", or "#".'
|
|
3028
|
+
},
|
|
3029
|
+
prevHash: {
|
|
3030
|
+
type: "string",
|
|
3031
|
+
description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
3032
|
+
}
|
|
3033
|
+
},
|
|
3034
|
+
required: ["path", "oldHeading", "newHeading"]
|
|
3035
|
+
}
|
|
3036
|
+
},
|
|
2805
3037
|
{
|
|
2806
3038
|
name: "append_note",
|
|
3039
|
+
annotations: ADDITIVE_WRITE_ANNOTATIONS,
|
|
2807
3040
|
description: "Append text to a note body without touching the frontmatter. Safe for meeting notes, daily logs, and append-only workflows.",
|
|
2808
3041
|
inputSchema: {
|
|
2809
3042
|
type: "object",
|
|
@@ -2820,6 +3053,7 @@ var ALL_TOOLS = [
|
|
|
2820
3053
|
},
|
|
2821
3054
|
{
|
|
2822
3055
|
name: "patch_frontmatter",
|
|
3056
|
+
annotations: IDEMPOTENT_DESTRUCTIVE_WRITE_ANNOTATIONS,
|
|
2823
3057
|
description: "Set, update, or delete frontmatter keys without reordering existing keys or changing quote style. Pass null as a value to delete a key.",
|
|
2824
3058
|
inputSchema: {
|
|
2825
3059
|
type: "object",
|
|
@@ -2840,6 +3074,7 @@ var ALL_TOOLS = [
|
|
|
2840
3074
|
},
|
|
2841
3075
|
{
|
|
2842
3076
|
name: "outline_note",
|
|
3077
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
2843
3078
|
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.",
|
|
2844
3079
|
inputSchema: {
|
|
2845
3080
|
type: "object",
|
|
@@ -2859,6 +3094,7 @@ var ALL_TOOLS = [
|
|
|
2859
3094
|
},
|
|
2860
3095
|
{
|
|
2861
3096
|
name: "patch_note",
|
|
3097
|
+
annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
|
|
2862
3098
|
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.",
|
|
2863
3099
|
inputSchema: {
|
|
2864
3100
|
type: "object",
|
|
@@ -2888,6 +3124,7 @@ var ALL_TOOLS = [
|
|
|
2888
3124
|
},
|
|
2889
3125
|
{
|
|
2890
3126
|
name: "get_backlinks",
|
|
3127
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
2891
3128
|
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.",
|
|
2892
3129
|
inputSchema: {
|
|
2893
3130
|
type: "object",
|
|
@@ -2907,6 +3144,7 @@ var ALL_TOOLS = [
|
|
|
2907
3144
|
},
|
|
2908
3145
|
{
|
|
2909
3146
|
name: "get_links",
|
|
3147
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
2910
3148
|
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.",
|
|
2911
3149
|
inputSchema: {
|
|
2912
3150
|
type: "object",
|
|
@@ -2918,6 +3156,7 @@ var ALL_TOOLS = [
|
|
|
2918
3156
|
},
|
|
2919
3157
|
{
|
|
2920
3158
|
name: "replace_in_note",
|
|
3159
|
+
annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
|
|
2921
3160
|
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.",
|
|
2922
3161
|
inputSchema: {
|
|
2923
3162
|
type: "object",
|
|
@@ -2958,6 +3197,7 @@ var ALL_TOOLS = [
|
|
|
2958
3197
|
},
|
|
2959
3198
|
{
|
|
2960
3199
|
name: "get_periodic_note",
|
|
3200
|
+
annotations: ADDITIVE_WRITE_ANNOTATIONS,
|
|
2961
3201
|
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.",
|
|
2962
3202
|
inputSchema: {
|
|
2963
3203
|
type: "object",
|
|
@@ -2981,6 +3221,7 @@ var ALL_TOOLS = [
|
|
|
2981
3221
|
},
|
|
2982
3222
|
{
|
|
2983
3223
|
name: "append_periodic_note",
|
|
3224
|
+
annotations: ADDITIVE_WRITE_ANNOTATIONS,
|
|
2984
3225
|
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).",
|
|
2985
3226
|
inputSchema: {
|
|
2986
3227
|
type: "object",
|
|
@@ -3018,8 +3259,8 @@ function visibleTools(policy2) {
|
|
|
3018
3259
|
|
|
3019
3260
|
// src/watcher.ts
|
|
3020
3261
|
import { realpathSync } from "fs";
|
|
3021
|
-
import { readFile as
|
|
3022
|
-
import { join as
|
|
3262
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3263
|
+
import { join as join7, relative as relative2, sep as sep3 } from "path";
|
|
3023
3264
|
import chokidar from "chokidar";
|
|
3024
3265
|
function startWatcher(ctx2, log2, opts) {
|
|
3025
3266
|
const usePolling = opts?.usePolling ?? process.env.SEEKSTONE_WATCH_POLL === "1";
|
|
@@ -3031,7 +3272,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
3031
3272
|
const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
|
|
3032
3273
|
async function upsert(relPath, op) {
|
|
3033
3274
|
try {
|
|
3034
|
-
const raw = await
|
|
3275
|
+
const raw = await readFile12(join7(vaultRoot2, relPath), "utf8");
|
|
3035
3276
|
removeNoteBacklinks(ctx2, relPath);
|
|
3036
3277
|
upsertDoc(ctx2, buildDoc(relPath, raw));
|
|
3037
3278
|
addNoteBacklinks(ctx2, relPath, raw);
|
|
@@ -3083,7 +3324,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
3083
3324
|
}
|
|
3084
3325
|
|
|
3085
3326
|
// src/index.ts
|
|
3086
|
-
var VERSION = true ? "0.
|
|
3327
|
+
var VERSION = true ? "0.13.1" : "0.0.0-dev";
|
|
3087
3328
|
var intent = parseCliIntent(process.argv.slice(2));
|
|
3088
3329
|
if (intent === "version") {
|
|
3089
3330
|
process.stdout.write(`${VERSION}
|