seekstone 0.4.1 → 0.6.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 CHANGED
@@ -746,30 +746,181 @@ function updateCache(ctx2, path2, newContent, bodyStart) {
746
746
  }
747
747
  }
748
748
 
749
- // src/tools/read_note.ts
750
- import { readFile as readFile5 } from "fs/promises";
751
- import { join as join10 } from "path";
749
+ // src/tools/periodic_note.ts
750
+ import { access as access3, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
751
+ import { dirname as dirname3, join as join10 } from "path";
752
752
  import { z as z12 } from "zod";
753
- var ReadNoteInput = z12.object({
754
- path: z12.string().describe('Vault-relative path to the note, e.g. "Daily Notes/2026-05-29.md".'),
755
- section: z12.string().optional().describe("Return only this heading section (heading text, # prefix optional)."),
756
- block: z12.string().optional().describe("Return only the block anchored by this id (^ prefix optional)."),
757
- lines: z12.object({
758
- from: z12.number().int().min(1).describe("First line (1-indexed, inclusive)."),
759
- to: z12.number().int().min(1).optional().describe("Last line (1-indexed, inclusive). Defaults to end of file.")
753
+ var PERIOD = z12.enum(["daily", "weekly", "monthly", "quarterly", "yearly"]);
754
+ var DEFAULTS = {
755
+ daily: { folder: "", format: "YYYY-MM-DD", template: "" },
756
+ weekly: { folder: "", format: "gggg-[W]ww", template: "" },
757
+ monthly: { folder: "", format: "YYYY-MM", template: "" },
758
+ quarterly: { folder: "", format: "YYYY-[Q]Q", template: "" },
759
+ yearly: { folder: "", format: "YYYY", template: "" }
760
+ };
761
+ async function readJson(absPath) {
762
+ try {
763
+ return JSON.parse(await readFile5(absPath, "utf8"));
764
+ } catch {
765
+ return null;
766
+ }
767
+ }
768
+ async function resolveConfig(vaultRoot2, period) {
769
+ const defaults = DEFAULTS[period];
770
+ if (period === "daily") {
771
+ const cfg2 = await readJson(join10(vaultRoot2, ".obsidian", "daily-notes.json"));
772
+ return {
773
+ folder: cfg2?.folder ?? defaults.folder,
774
+ format: cfg2?.format ?? defaults.format,
775
+ template: cfg2?.template ?? defaults.template
776
+ };
777
+ }
778
+ const cfg = await readJson(
779
+ join10(vaultRoot2, ".obsidian", "plugins", "periodic-notes", "data.json")
780
+ );
781
+ const entry = cfg?.[period];
782
+ return {
783
+ folder: entry?.folder ?? defaults.folder,
784
+ format: entry?.format ?? defaults.format,
785
+ template: entry?.template ?? defaults.template
786
+ };
787
+ }
788
+ function isoWeekAndYear(date) {
789
+ const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
790
+ const day = d.getUTCDay() || 7;
791
+ d.setUTCDate(d.getUTCDate() + 4 - day);
792
+ const jan1 = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
793
+ const week = Math.ceil(((d.getTime() - jan1.getTime()) / 864e5 + 1) / 7);
794
+ return { week, year: d.getUTCFullYear() };
795
+ }
796
+ function formatMomentDate(date, format) {
797
+ const year = date.getUTCFullYear();
798
+ const month = date.getUTCMonth() + 1;
799
+ const day = date.getUTCDate();
800
+ const quarter = Math.ceil(month / 3);
801
+ const { week: isoWeek, year: isoYear } = isoWeekAndYear(date);
802
+ const PLACEHOLDER = "\0";
803
+ const literals = [];
804
+ let s = format.replace(/\[([^\]]*)\]/g, (_, lit) => {
805
+ literals.push(lit);
806
+ return `${PLACEHOLDER}${literals.length - 1}${PLACEHOLDER}`;
807
+ });
808
+ s = s.replace(/gggg/g, String(isoYear).padStart(4, "0")).replace(/YYYY/g, String(year).padStart(4, "0")).replace(/YY/g, String(year).slice(-2)).replace(/MM/g, String(month).padStart(2, "0")).replace(/M/g, String(month)).replace(/DD/g, String(day).padStart(2, "0")).replace(/D/g, String(day)).replace(/ww/g, String(isoWeek).padStart(2, "0")).replace(/W/g, String(isoWeek)).replace(/Q/g, String(quarter));
809
+ return s.replace(
810
+ new RegExp(`${PLACEHOLDER}(\\d+)${PLACEHOLDER}`, "g"),
811
+ (_, i) => literals[+i] ?? ""
812
+ );
813
+ }
814
+ function resolveNotePath(cfg, date) {
815
+ const filename = `${formatMomentDate(date, cfg.format)}.md`;
816
+ return cfg.folder ? `${cfg.folder}/${filename}` : filename;
817
+ }
818
+ var GetPeriodicNoteInput = z12.object({
819
+ period: PERIOD.default("daily").describe(
820
+ "Period type: daily (default), weekly, monthly, quarterly, or yearly."
821
+ ),
822
+ date: z12.string().optional().describe("ISO date string (YYYY-MM-DD). Defaults to today when omitted."),
823
+ createIfMissing: z12.boolean().default(false).describe(
824
+ "Create the note from the configured template if it does not exist. Default false \u2014 reports missing without creating."
825
+ )
826
+ });
827
+ async function getPeriodicNote(ctx2, rawInput) {
828
+ const input = GetPeriodicNoteInput.parse(rawInput);
829
+ const date = input.date ? new Date(input.date) : /* @__PURE__ */ new Date();
830
+ const cfg = await resolveConfig(ctx2.vaultRoot, input.period);
831
+ const path2 = resolveNotePath(cfg, date);
832
+ const abs = join10(ctx2.vaultRoot, path2);
833
+ if (!abs.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${path2}`);
834
+ let existed = false;
835
+ try {
836
+ await access3(abs);
837
+ existed = true;
838
+ } catch {
839
+ }
840
+ if (existed) return { path: path2, existed: true, created: false };
841
+ if (!input.createIfMissing) return { path: path2, existed: false, created: false };
842
+ let body = "";
843
+ if (cfg.template) {
844
+ const templateAbs = join10(
845
+ ctx2.vaultRoot,
846
+ cfg.template.endsWith(".md") ? cfg.template : `${cfg.template}.md`
847
+ );
848
+ if (templateAbs.startsWith(ctx2.vaultRoot)) {
849
+ try {
850
+ body = await readFile5(templateAbs, "utf8");
851
+ } catch {
852
+ }
853
+ }
854
+ }
855
+ await mkdir3(dirname3(abs), { recursive: true });
856
+ await writeFile5(abs, body, "utf8");
857
+ upsertDoc(ctx2, buildDoc(path2, body));
858
+ return { path: path2, existed: false, created: true };
859
+ }
860
+ var AppendPeriodicNoteInput = z12.object({
861
+ period: PERIOD.default("daily").describe("Period type. Default: daily."),
862
+ date: z12.string().optional().describe("ISO date string (YYYY-MM-DD). Defaults to today."),
863
+ content: z12.string().min(1).describe("Text to append to the note body. Separated from existing content by a blank line."),
864
+ createIfMissing: z12.boolean().default(true).describe("Create the note if it does not exist before appending. Default true.")
865
+ });
866
+ async function appendPeriodicNote(ctx2, rawInput) {
867
+ const input = AppendPeriodicNoteInput.parse(rawInput);
868
+ const date = input.date ? new Date(input.date) : /* @__PURE__ */ new Date();
869
+ const cfg = await resolveConfig(ctx2.vaultRoot, input.period);
870
+ const path2 = resolveNotePath(cfg, date);
871
+ const abs = join10(ctx2.vaultRoot, path2);
872
+ if (!abs.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${path2}`);
873
+ let original = "";
874
+ try {
875
+ original = await readFile5(abs, "utf8");
876
+ } catch (err) {
877
+ if (err.code !== "ENOENT") throw err;
878
+ if (!input.createIfMissing) throw new Error(`Periodic note not found: ${path2}`);
879
+ await mkdir3(dirname3(abs), { recursive: true });
880
+ await writeFile5(abs, input.content, "utf8");
881
+ upsertDoc(ctx2, buildDoc(path2, input.content));
882
+ return { path: path2, bytesWritten: Buffer.byteLength(input.content, "utf8") };
883
+ }
884
+ const fm = parseFrontmatter(original);
885
+ const body = fm.body.endsWith("\n") ? fm.body : `${fm.body}
886
+ `;
887
+ const separator = body.endsWith("\n\n") ? "" : "\n";
888
+ const newBody = `${body}${separator}${input.content}`;
889
+ const header = original.slice(0, fm.bodyStart);
890
+ const newRaw = `${header}${newBody}`;
891
+ await writeFile5(abs, newRaw, "utf8");
892
+ const cached = ctx2.notes.get(path2);
893
+ if (cached) {
894
+ cached.body = newBody;
895
+ cached.raw = newRaw;
896
+ }
897
+ return { path: path2, bytesWritten: Buffer.byteLength(newRaw, "utf8") };
898
+ }
899
+
900
+ // src/tools/read_note.ts
901
+ import { readFile as readFile6 } from "fs/promises";
902
+ import { join as join11 } from "path";
903
+ import { z as z13 } from "zod";
904
+ var ReadNoteInput = z13.object({
905
+ path: z13.string().describe('Vault-relative path to the note, e.g. "Daily Notes/2026-05-29.md".'),
906
+ section: z13.string().optional().describe("Return only this heading section (heading text, # prefix optional)."),
907
+ block: z13.string().optional().describe("Return only the block anchored by this id (^ prefix optional)."),
908
+ lines: z13.object({
909
+ from: z13.number().int().min(1).describe("First line (1-indexed, inclusive)."),
910
+ to: z13.number().int().min(1).optional().describe("Last line (1-indexed, inclusive). Defaults to end of file.")
760
911
  }).optional().describe("Return only this line range (1-indexed, inclusive)."),
761
- includeFrontmatter: z12.boolean().optional().describe(
912
+ includeFrontmatter: z13.boolean().optional().describe(
762
913
  "Include frontmatter in span results. Default false for section/block spans. No effect on lines or whole-note reads."
763
914
  )
764
915
  }).refine((v) => [v.section, v.block, v.lines].filter((x) => x !== void 0).length <= 1, {
765
916
  message: "At most one of section, block, or lines may be specified."
766
917
  });
767
918
  async function readNote(ctx2, input) {
768
- const absPath = join10(ctx2.vaultRoot, input.path);
919
+ const absPath = join11(ctx2.vaultRoot, input.path);
769
920
  if (!absPath.startsWith(ctx2.vaultRoot)) {
770
921
  throw new Error(`Path outside vault: ${input.path}`);
771
922
  }
772
- const raw = await readFile5(absPath, "utf8");
923
+ const raw = await readFile6(absPath, "utf8");
773
924
  const noteBytes = Buffer.byteLength(raw, "utf8");
774
925
  if (input.section === void 0 && input.block === void 0 && input.lines === void 0) {
775
926
  return { path: input.path, content: raw, bytesReturned: noteBytes, noteBytes };
@@ -848,8 +999,101 @@ async function readNote(ctx2, input) {
848
999
  };
849
1000
  }
850
1001
 
1002
+ // src/tools/replace_in_note.ts
1003
+ import { readFile as readFile7, rename as rename3, writeFile as writeFile6 } from "fs/promises";
1004
+ import { join as join12 } from "path";
1005
+ import { z as z14 } from "zod";
1006
+ var ReplaceInNoteInput = z14.object({
1007
+ path: z14.string().describe("Vault-relative path to the note."),
1008
+ find: z14.string().min(1).describe("Text or pattern to find."),
1009
+ replace: z14.string().describe("Replacement text. Supports $1, $2, \u2026 backreferences in regex mode."),
1010
+ regex: z14.boolean().default(false).describe("Treat find as a regular expression."),
1011
+ caseSensitive: z14.boolean().default(false).describe("Case-sensitive matching."),
1012
+ wholeWord: z14.boolean().default(false).describe("Match whole words only (\\b word-boundary)."),
1013
+ limit: z14.number().int().min(1).optional().describe("Maximum number of replacements. Omit to replace all."),
1014
+ dryRun: z14.boolean().default(false).describe("If true, report matches without writing anything.")
1015
+ });
1016
+ function escapeRegex(s) {
1017
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1018
+ }
1019
+ function buildRegex(find, opts) {
1020
+ let pattern = opts.regex ? find : escapeRegex(find);
1021
+ if (opts.wholeWord) pattern = `\\b(?:${pattern})\\b`;
1022
+ const flags = `g${opts.caseSensitive ? "" : "i"}`;
1023
+ return new RegExp(pattern, flags);
1024
+ }
1025
+ async function atomicWrite2(absPath, content) {
1026
+ const tmpPath = `${absPath}.seekstone-replace-tmp`;
1027
+ await writeFile6(tmpPath, content, "utf8");
1028
+ await rename3(tmpPath, absPath);
1029
+ }
1030
+ async function replaceInNote(ctx2, input) {
1031
+ const absPath = join12(ctx2.vaultRoot, input.path);
1032
+ if (!absPath.startsWith(ctx2.vaultRoot)) {
1033
+ throw new Error(`Path outside vault: ${input.path}`);
1034
+ }
1035
+ let re;
1036
+ try {
1037
+ re = buildRegex(input.find, {
1038
+ regex: input.regex,
1039
+ caseSensitive: input.caseSensitive,
1040
+ wholeWord: input.wholeWord
1041
+ });
1042
+ } catch (err) {
1043
+ throw new Error(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`);
1044
+ }
1045
+ const raw = await readFile7(absPath, "utf8");
1046
+ const fm = parseFrontmatter(raw);
1047
+ const originalFmRegion = raw.slice(0, fm.bodyStart);
1048
+ const body = raw.slice(fm.bodyStart);
1049
+ const allMatches = [...body.matchAll(re)];
1050
+ const toReplace = input.limit !== void 0 ? allMatches.slice(0, input.limit) : allMatches;
1051
+ const matches = toReplace.map((m) => {
1052
+ const fileOffset = fm.bodyStart + (m.index ?? 0);
1053
+ const line = raw.slice(0, fileOffset).split("\n").length;
1054
+ return { line, byteOffset: fileOffset };
1055
+ });
1056
+ if (input.dryRun || toReplace.length === 0) {
1057
+ return {
1058
+ path: input.path,
1059
+ replacements: toReplace.length,
1060
+ matches,
1061
+ frontmatterUnchanged: true
1062
+ };
1063
+ }
1064
+ const singleRe = new RegExp(re.source, re.flags.replace("g", ""));
1065
+ let newBody = "";
1066
+ let lastIndex = 0;
1067
+ for (const m of toReplace) {
1068
+ const matchIndex = m.index ?? 0;
1069
+ newBody += body.slice(lastIndex, matchIndex);
1070
+ newBody += input.regex ? m[0].replace(singleRe, input.replace) : input.replace;
1071
+ lastIndex = matchIndex + m[0].length;
1072
+ }
1073
+ newBody += body.slice(lastIndex);
1074
+ const newContent = `${originalFmRegion}${newBody}`;
1075
+ await atomicWrite2(absPath, newContent);
1076
+ const written = await readFile7(absPath, "utf8");
1077
+ if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
1078
+ throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
1079
+ }
1080
+ const cached = ctx2.notes.get(input.path);
1081
+ if (cached) {
1082
+ cached.body = newBody;
1083
+ cached.raw = newContent;
1084
+ cached.sizeBytes = Buffer.byteLength(newContent, "utf8");
1085
+ }
1086
+ return {
1087
+ path: input.path,
1088
+ replacements: toReplace.length,
1089
+ matches,
1090
+ bytesWritten: Buffer.byteLength(newContent, "utf8"),
1091
+ frontmatterUnchanged: true
1092
+ };
1093
+ }
1094
+
851
1095
  // src/tools/search.ts
852
- import { z as z13 } from "zod";
1096
+ import { z as z15 } from "zod";
853
1097
 
854
1098
  // src/index/excerpt.ts
855
1099
  function extractExcerpt(text, terms, maxLen = 200) {
@@ -876,11 +1120,11 @@ function truncate(s, max) {
876
1120
  }
877
1121
 
878
1122
  // src/tools/search.ts
879
- var SearchInput = z13.object({
880
- query: z13.string().min(1).describe("Search query. Supports fuzzy matching and prefix search."),
881
- limit: z13.number().int().min(1).max(50).default(10).describe("Maximum number of results to return."),
882
- folder: z13.string().optional().describe("Restrict results to notes under this vault-relative folder prefix."),
883
- tag: z13.string().optional().describe("Restrict results to notes containing this tag.")
1123
+ var SearchInput = z15.object({
1124
+ query: z15.string().min(1).describe("Search query. Supports fuzzy matching and prefix search."),
1125
+ limit: z15.number().int().min(1).max(50).default(10).describe("Maximum number of results to return."),
1126
+ folder: z15.string().optional().describe("Restrict results to notes under this vault-relative folder prefix."),
1127
+ tag: z15.string().optional().describe("Restrict results to notes containing this tag.")
884
1128
  });
885
1129
  function search(ctx2, input) {
886
1130
  const results = ctx2.index.search(input.query, {
@@ -924,7 +1168,9 @@ var META_KEYS = [
924
1168
  "overwrite",
925
1169
  "pattern",
926
1170
  "minCount",
927
- "sort"
1171
+ "sort",
1172
+ "period",
1173
+ "date"
928
1174
  ];
929
1175
  function safeMeta(args) {
930
1176
  if (typeof args !== "object" || args === null) return {};
@@ -1036,18 +1282,40 @@ async function run(ctx2, name, args) {
1036
1282
  const result = getLinks(ctx2, input);
1037
1283
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1038
1284
  }
1285
+ case "replace_in_note": {
1286
+ const input = ReplaceInNoteInput.parse(args);
1287
+ const result = await replaceInNote(ctx2, input);
1288
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1289
+ }
1290
+ case "get_periodic_note": {
1291
+ const input = GetPeriodicNoteInput.parse(args);
1292
+ const result = await getPeriodicNote(ctx2, input);
1293
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1294
+ }
1295
+ case "append_periodic_note": {
1296
+ const input = AppendPeriodicNoteInput.parse(args);
1297
+ const result = await appendPeriodicNote(ctx2, input);
1298
+ return {
1299
+ content: [
1300
+ {
1301
+ type: "text",
1302
+ text: `Appended ${result.bytesWritten} bytes to ${result.path}.`
1303
+ }
1304
+ ]
1305
+ };
1306
+ }
1039
1307
  default:
1040
1308
  throw new Error(`Unknown tool: ${name}`);
1041
1309
  }
1042
1310
  }
1043
1311
 
1044
1312
  // src/index/build.ts
1045
- import { readFile as readFile6 } from "fs/promises";
1313
+ import { readFile as readFile8 } from "fs/promises";
1046
1314
  import { basename as basename2, extname as extname3 } from "path";
1047
1315
 
1048
1316
  // ../core/src/walk.ts
1049
1317
  import { stat } from "fs/promises";
1050
- import { extname as extname2, join as join11, relative, sep } from "path";
1318
+ import { extname as extname2, join as join13, relative, sep } from "path";
1051
1319
  import fg from "fast-glob";
1052
1320
  var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".heic"]);
1053
1321
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
@@ -1124,7 +1392,7 @@ async function buildIndex(vaultRoot2) {
1124
1392
  await Promise.all(
1125
1393
  noteEntries.map(async (entry) => {
1126
1394
  try {
1127
- const raw = await readFile6(entry.absPath, "utf8");
1395
+ const raw = await readFile8(entry.absPath, "utf8");
1128
1396
  const fm = parseFrontmatter(raw);
1129
1397
  const inlineTags = extractInlineTags(fm.body);
1130
1398
  const fmTags = frontmatterTags(fm.data);
@@ -1176,8 +1444,8 @@ function buildBacklinks(notes2) {
1176
1444
 
1177
1445
  // src/init.ts
1178
1446
  import { execFileSync } from "child_process";
1179
- import { access as access3, copyFile, mkdir as mkdir3, readFile as readFile7, readdir, stat as stat2, writeFile as writeFile5 } from "fs/promises";
1180
- import path, { dirname as dirname3, join as join12 } from "path";
1447
+ import { access as access4, copyFile, mkdir as mkdir4, readFile as readFile9, readdir, stat as stat2, writeFile as writeFile7 } from "fs/promises";
1448
+ import path, { dirname as dirname4, join as join14 } from "path";
1181
1449
  function parseInitArgs(argv) {
1182
1450
  const opts = { write: false, client: "desktop" };
1183
1451
  for (let i = 0; i < argv.length; i++) {
@@ -1265,7 +1533,7 @@ async function validateVault(vaultPath) {
1265
1533
  return { ok: false, error: `Path does not exist: ${vaultPath}` };
1266
1534
  }
1267
1535
  try {
1268
- await access3(join12(vaultPath, ".obsidian"));
1536
+ await access4(join14(vaultPath, ".obsidian"));
1269
1537
  } catch {
1270
1538
  return {
1271
1539
  ok: false,
@@ -1285,7 +1553,7 @@ async function countMarkdown(root) {
1285
1553
  }
1286
1554
  for (const e of entries) {
1287
1555
  if (e.name.startsWith(".")) continue;
1288
- const full = join12(dir, e.name);
1556
+ const full = join14(dir, e.name);
1289
1557
  if (e.isDirectory()) await walk(full);
1290
1558
  else if (e.name.endsWith(".md")) count++;
1291
1559
  }
@@ -1294,7 +1562,7 @@ async function countMarkdown(root) {
1294
1562
  return count;
1295
1563
  }
1296
1564
  async function runInit(opts, deps) {
1297
- const rf = deps.readFile ?? readFile7;
1565
+ const rf = deps.readFile ?? readFile9;
1298
1566
  let vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT;
1299
1567
  if (!vaultPath) {
1300
1568
  const registryPath = obsidianRegistryPath(deps.platform, {
@@ -1423,12 +1691,12 @@ async function runInit(opts, deps) {
1423
1691
  }
1424
1692
  const merged = mergeSeekstoneConfig(existing, vaultPath);
1425
1693
  let backupPath;
1426
- await mkdir3(dirname3(configPath), { recursive: true });
1694
+ await mkdir4(dirname4(configPath), { recursive: true });
1427
1695
  if (existingRaw !== null) {
1428
1696
  backupPath = `${configPath}.backup-${deps.timestamp}`;
1429
1697
  await copyFile(configPath, backupPath);
1430
1698
  }
1431
- await writeFile5(configPath, `${JSON.stringify(merged, null, 2)}
1699
+ await writeFile7(configPath, `${JSON.stringify(merged, null, 2)}
1432
1700
  `, "utf8");
1433
1701
  out.push(
1434
1702
  `\u2713 Patched ${configPath}`,
@@ -1500,7 +1768,7 @@ function createLogger(opts = {}) {
1500
1768
  );
1501
1769
  }
1502
1770
  }
1503
- function writeFile6(record) {
1771
+ function writeFile8(record) {
1504
1772
  if (!fileOk || filePath === void 0) return;
1505
1773
  const line = `${JSON.stringify(record)}
1506
1774
  `;
@@ -1522,7 +1790,7 @@ function createLogger(opts = {}) {
1522
1790
  const f = fields ?? {};
1523
1791
  stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
1524
1792
  `);
1525
- writeFile6({ t: ts, level: lvl, msg, ...f });
1793
+ writeFile8({ t: ts, level: lvl, msg, ...f });
1526
1794
  }
1527
1795
  return {
1528
1796
  level,
@@ -1534,8 +1802,8 @@ function createLogger(opts = {}) {
1534
1802
  }
1535
1803
 
1536
1804
  // src/watcher.ts
1537
- import { readFile as readFile8 } from "fs/promises";
1538
- import { join as join13, relative as relative2, sep as sep2 } from "path";
1805
+ import { readFile as readFile10 } from "fs/promises";
1806
+ import { join as join15, relative as relative2, sep as sep2 } from "path";
1539
1807
  import chokidar from "chokidar";
1540
1808
  function removeNoteBacklinks(ctx2, relPath) {
1541
1809
  const oldDoc = ctx2.notes.get(relPath);
@@ -1572,7 +1840,7 @@ function startWatcher(ctx2, log2, opts) {
1572
1840
  const toRel = (abs) => relative2(ctx2.vaultRoot, abs).split(sep2).join("/");
1573
1841
  async function upsert(relPath, op) {
1574
1842
  try {
1575
- const raw = await readFile8(join13(ctx2.vaultRoot, relPath), "utf8");
1843
+ const raw = await readFile10(join15(ctx2.vaultRoot, relPath), "utf8");
1576
1844
  removeNoteBacklinks(ctx2, relPath);
1577
1845
  upsertDoc(ctx2, buildDoc(relPath, raw));
1578
1846
  addNoteBacklinks(ctx2, relPath, raw);
@@ -1590,6 +1858,7 @@ function startWatcher(ctx2, log2, opts) {
1590
1858
  }
1591
1859
  const onUpsert = (op) => (abs) => {
1592
1860
  const relPath = toRel(abs);
1861
+ opts?.onRawEvent?.(op, abs, relPath);
1593
1862
  if (relPath.endsWith(".md")) void upsert(relPath, op);
1594
1863
  };
1595
1864
  const watcher2 = chokidar.watch(ctx2.vaultRoot, {
@@ -1608,6 +1877,7 @@ function startWatcher(ctx2, log2, opts) {
1608
1877
  });
1609
1878
  watcher2.on("add", onUpsert("add")).on("change", onUpsert("change")).on("unlink", (abs) => {
1610
1879
  const relPath = toRel(abs);
1880
+ opts?.onRawEvent?.("unlink", abs, relPath);
1611
1881
  if (relPath.endsWith(".md")) remove(relPath);
1612
1882
  });
1613
1883
  const ready = new Promise((resolve) => {
@@ -1620,7 +1890,7 @@ function startWatcher(ctx2, log2, opts) {
1620
1890
  }
1621
1891
 
1622
1892
  // src/index.ts
1623
- var VERSION = true ? "0.4.1" : "0.0.0-dev";
1893
+ var VERSION = true ? "0.6.0" : "0.0.0-dev";
1624
1894
  var intent = parseCliIntent(process.argv.slice(2));
1625
1895
  if (intent === "version") {
1626
1896
  process.stdout.write(`${VERSION}
@@ -1897,6 +2167,92 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1897
2167
  },
1898
2168
  required: ["path"]
1899
2169
  }
2170
+ },
2171
+ {
2172
+ name: "replace_in_note",
2173
+ 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.",
2174
+ inputSchema: {
2175
+ type: "object",
2176
+ properties: {
2177
+ path: { type: "string", description: "Vault-relative path to the note." },
2178
+ find: { type: "string", description: "Text or pattern to find." },
2179
+ replace: {
2180
+ type: "string",
2181
+ description: "Replacement text. Supports $1, $2, \u2026 backreferences in regex mode."
2182
+ },
2183
+ regex: {
2184
+ type: "boolean",
2185
+ description: "Treat find as a regular expression. Default false."
2186
+ },
2187
+ caseSensitive: {
2188
+ type: "boolean",
2189
+ description: "Case-sensitive matching. Default false."
2190
+ },
2191
+ wholeWord: {
2192
+ type: "boolean",
2193
+ description: "Match whole words only (\\b boundary). Default false."
2194
+ },
2195
+ limit: {
2196
+ type: "number",
2197
+ description: "Maximum number of replacements. Omit to replace all."
2198
+ },
2199
+ dryRun: {
2200
+ type: "boolean",
2201
+ description: "If true, report matches without writing. Default false."
2202
+ }
2203
+ },
2204
+ required: ["path", "find", "replace"]
2205
+ }
2206
+ },
2207
+ {
2208
+ name: "get_periodic_note",
2209
+ 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.",
2210
+ inputSchema: {
2211
+ type: "object",
2212
+ properties: {
2213
+ period: {
2214
+ type: "string",
2215
+ enum: ["daily", "weekly", "monthly", "quarterly", "yearly"],
2216
+ description: "Period type. Default: daily."
2217
+ },
2218
+ date: {
2219
+ type: "string",
2220
+ description: "ISO date string (YYYY-MM-DD). Defaults to today when omitted."
2221
+ },
2222
+ createIfMissing: {
2223
+ type: "boolean",
2224
+ description: "Create the note from the configured template if it does not exist. Default false."
2225
+ }
2226
+ },
2227
+ required: []
2228
+ }
2229
+ },
2230
+ {
2231
+ name: "append_periodic_note",
2232
+ 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).",
2233
+ inputSchema: {
2234
+ type: "object",
2235
+ properties: {
2236
+ period: {
2237
+ type: "string",
2238
+ enum: ["daily", "weekly", "monthly", "quarterly", "yearly"],
2239
+ description: "Period type. Default: daily."
2240
+ },
2241
+ date: {
2242
+ type: "string",
2243
+ description: "ISO date string (YYYY-MM-DD). Defaults to today when omitted."
2244
+ },
2245
+ content: {
2246
+ type: "string",
2247
+ description: "Text to append to the note body."
2248
+ },
2249
+ createIfMissing: {
2250
+ type: "boolean",
2251
+ description: "Create the note if it does not exist before appending. Default true."
2252
+ }
2253
+ },
2254
+ required: ["content"]
2255
+ }
1900
2256
  }
1901
2257
  ]
1902
2258
  }));
@@ -1906,7 +2262,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1906
2262
  });
1907
2263
  var transport = new StdioServerTransport();
1908
2264
  await server.connect(transport);
1909
- log.info("ready", { tools: 13, transport: "stdio" });
2265
+ log.info("ready", { tools: 16, transport: "stdio" });
1910
2266
  process.stderr.write(
1911
2267
  `seekstone: add to Claude Desktop:
1912
2268
  ${JSON.stringify(