seekstone 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/index.js +250 -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, {
|
|
@@ -2802,6 +2976,32 @@ var ALL_TOOLS = [
|
|
|
2802
2976
|
required: ["from", "to"]
|
|
2803
2977
|
}
|
|
2804
2978
|
},
|
|
2979
|
+
{
|
|
2980
|
+
name: "rename_heading",
|
|
2981
|
+
description: "Rename a heading in a note and rewrite every [[note#heading]] wikilink and embed across the vault so references keep working \u2014 aliases preserved, fenced code blocks left alone. Served from the warm backlink index, no vault scan. Heading matching is case-insensitive; with duplicate headings the first match wins, mirroring Obsidian link resolution.",
|
|
2982
|
+
inputSchema: {
|
|
2983
|
+
type: "object",
|
|
2984
|
+
properties: {
|
|
2985
|
+
path: {
|
|
2986
|
+
type: "string",
|
|
2987
|
+
description: "Vault-relative path of the note containing the heading."
|
|
2988
|
+
},
|
|
2989
|
+
oldHeading: {
|
|
2990
|
+
type: "string",
|
|
2991
|
+
description: "Current heading text, without # markers. Case-insensitive; first match wins."
|
|
2992
|
+
},
|
|
2993
|
+
newHeading: {
|
|
2994
|
+
type: "string",
|
|
2995
|
+
description: 'New heading text, without # markers. Must not contain newlines, "[", "]", "|", or "#".'
|
|
2996
|
+
},
|
|
2997
|
+
prevHash: {
|
|
2998
|
+
type: "string",
|
|
2999
|
+
description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
3000
|
+
}
|
|
3001
|
+
},
|
|
3002
|
+
required: ["path", "oldHeading", "newHeading"]
|
|
3003
|
+
}
|
|
3004
|
+
},
|
|
2805
3005
|
{
|
|
2806
3006
|
name: "append_note",
|
|
2807
3007
|
description: "Append text to a note body without touching the frontmatter. Safe for meeting notes, daily logs, and append-only workflows.",
|
|
@@ -3018,8 +3218,8 @@ function visibleTools(policy2) {
|
|
|
3018
3218
|
|
|
3019
3219
|
// src/watcher.ts
|
|
3020
3220
|
import { realpathSync } from "fs";
|
|
3021
|
-
import { readFile as
|
|
3022
|
-
import { join as
|
|
3221
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3222
|
+
import { join as join7, relative as relative2, sep as sep3 } from "path";
|
|
3023
3223
|
import chokidar from "chokidar";
|
|
3024
3224
|
function startWatcher(ctx2, log2, opts) {
|
|
3025
3225
|
const usePolling = opts?.usePolling ?? process.env.SEEKSTONE_WATCH_POLL === "1";
|
|
@@ -3031,7 +3231,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
3031
3231
|
const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
|
|
3032
3232
|
async function upsert(relPath, op) {
|
|
3033
3233
|
try {
|
|
3034
|
-
const raw = await
|
|
3234
|
+
const raw = await readFile12(join7(vaultRoot2, relPath), "utf8");
|
|
3035
3235
|
removeNoteBacklinks(ctx2, relPath);
|
|
3036
3236
|
upsertDoc(ctx2, buildDoc(relPath, raw));
|
|
3037
3237
|
addNoteBacklinks(ctx2, relPath, raw);
|
|
@@ -3083,7 +3283,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
3083
3283
|
}
|
|
3084
3284
|
|
|
3085
3285
|
// src/index.ts
|
|
3086
|
-
var VERSION = true ? "0.
|
|
3286
|
+
var VERSION = true ? "0.13.0" : "0.0.0-dev";
|
|
3087
3287
|
var intent = parseCliIntent(process.argv.slice(2));
|
|
3088
3288
|
if (intent === "version") {
|
|
3089
3289
|
process.stdout.write(`${VERSION}
|