seekstone 0.5.0 → 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 };
@@ -849,18 +1000,18 @@ async function readNote(ctx2, input) {
849
1000
  }
850
1001
 
851
1002
  // src/tools/replace_in_note.ts
852
- import { readFile as readFile6, rename as rename3, writeFile as writeFile5 } from "fs/promises";
853
- import { join as join11 } from "path";
854
- import { z as z13 } from "zod";
855
- var ReplaceInNoteInput = z13.object({
856
- path: z13.string().describe("Vault-relative path to the note."),
857
- find: z13.string().min(1).describe("Text or pattern to find."),
858
- replace: z13.string().describe("Replacement text. Supports $1, $2, \u2026 backreferences in regex mode."),
859
- regex: z13.boolean().default(false).describe("Treat find as a regular expression."),
860
- caseSensitive: z13.boolean().default(false).describe("Case-sensitive matching."),
861
- wholeWord: z13.boolean().default(false).describe("Match whole words only (\\b word-boundary)."),
862
- limit: z13.number().int().min(1).optional().describe("Maximum number of replacements. Omit to replace all."),
863
- dryRun: z13.boolean().default(false).describe("If true, report matches without writing anything.")
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.")
864
1015
  });
865
1016
  function escapeRegex(s) {
866
1017
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -873,11 +1024,11 @@ function buildRegex(find, opts) {
873
1024
  }
874
1025
  async function atomicWrite2(absPath, content) {
875
1026
  const tmpPath = `${absPath}.seekstone-replace-tmp`;
876
- await writeFile5(tmpPath, content, "utf8");
1027
+ await writeFile6(tmpPath, content, "utf8");
877
1028
  await rename3(tmpPath, absPath);
878
1029
  }
879
1030
  async function replaceInNote(ctx2, input) {
880
- const absPath = join11(ctx2.vaultRoot, input.path);
1031
+ const absPath = join12(ctx2.vaultRoot, input.path);
881
1032
  if (!absPath.startsWith(ctx2.vaultRoot)) {
882
1033
  throw new Error(`Path outside vault: ${input.path}`);
883
1034
  }
@@ -891,7 +1042,7 @@ async function replaceInNote(ctx2, input) {
891
1042
  } catch (err) {
892
1043
  throw new Error(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`);
893
1044
  }
894
- const raw = await readFile6(absPath, "utf8");
1045
+ const raw = await readFile7(absPath, "utf8");
895
1046
  const fm = parseFrontmatter(raw);
896
1047
  const originalFmRegion = raw.slice(0, fm.bodyStart);
897
1048
  const body = raw.slice(fm.bodyStart);
@@ -922,7 +1073,7 @@ async function replaceInNote(ctx2, input) {
922
1073
  newBody += body.slice(lastIndex);
923
1074
  const newContent = `${originalFmRegion}${newBody}`;
924
1075
  await atomicWrite2(absPath, newContent);
925
- const written = await readFile6(absPath, "utf8");
1076
+ const written = await readFile7(absPath, "utf8");
926
1077
  if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
927
1078
  throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
928
1079
  }
@@ -942,7 +1093,7 @@ async function replaceInNote(ctx2, input) {
942
1093
  }
943
1094
 
944
1095
  // src/tools/search.ts
945
- import { z as z14 } from "zod";
1096
+ import { z as z15 } from "zod";
946
1097
 
947
1098
  // src/index/excerpt.ts
948
1099
  function extractExcerpt(text, terms, maxLen = 200) {
@@ -969,11 +1120,11 @@ function truncate(s, max) {
969
1120
  }
970
1121
 
971
1122
  // src/tools/search.ts
972
- var SearchInput = z14.object({
973
- query: z14.string().min(1).describe("Search query. Supports fuzzy matching and prefix search."),
974
- limit: z14.number().int().min(1).max(50).default(10).describe("Maximum number of results to return."),
975
- folder: z14.string().optional().describe("Restrict results to notes under this vault-relative folder prefix."),
976
- tag: z14.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.")
977
1128
  });
978
1129
  function search(ctx2, input) {
979
1130
  const results = ctx2.index.search(input.query, {
@@ -1017,7 +1168,9 @@ var META_KEYS = [
1017
1168
  "overwrite",
1018
1169
  "pattern",
1019
1170
  "minCount",
1020
- "sort"
1171
+ "sort",
1172
+ "period",
1173
+ "date"
1021
1174
  ];
1022
1175
  function safeMeta(args) {
1023
1176
  if (typeof args !== "object" || args === null) return {};
@@ -1134,18 +1287,35 @@ async function run(ctx2, name, args) {
1134
1287
  const result = await replaceInNote(ctx2, input);
1135
1288
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1136
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
+ }
1137
1307
  default:
1138
1308
  throw new Error(`Unknown tool: ${name}`);
1139
1309
  }
1140
1310
  }
1141
1311
 
1142
1312
  // src/index/build.ts
1143
- import { readFile as readFile7 } from "fs/promises";
1313
+ import { readFile as readFile8 } from "fs/promises";
1144
1314
  import { basename as basename2, extname as extname3 } from "path";
1145
1315
 
1146
1316
  // ../core/src/walk.ts
1147
1317
  import { stat } from "fs/promises";
1148
- import { extname as extname2, join as join12, relative, sep } from "path";
1318
+ import { extname as extname2, join as join13, relative, sep } from "path";
1149
1319
  import fg from "fast-glob";
1150
1320
  var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".heic"]);
1151
1321
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
@@ -1222,7 +1392,7 @@ async function buildIndex(vaultRoot2) {
1222
1392
  await Promise.all(
1223
1393
  noteEntries.map(async (entry) => {
1224
1394
  try {
1225
- const raw = await readFile7(entry.absPath, "utf8");
1395
+ const raw = await readFile8(entry.absPath, "utf8");
1226
1396
  const fm = parseFrontmatter(raw);
1227
1397
  const inlineTags = extractInlineTags(fm.body);
1228
1398
  const fmTags = frontmatterTags(fm.data);
@@ -1274,8 +1444,8 @@ function buildBacklinks(notes2) {
1274
1444
 
1275
1445
  // src/init.ts
1276
1446
  import { execFileSync } from "child_process";
1277
- import { access as access3, copyFile, mkdir as mkdir3, readFile as readFile8, readdir, stat as stat2, writeFile as writeFile6 } from "fs/promises";
1278
- import path, { dirname as dirname3, join as join13 } 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";
1279
1449
  function parseInitArgs(argv) {
1280
1450
  const opts = { write: false, client: "desktop" };
1281
1451
  for (let i = 0; i < argv.length; i++) {
@@ -1363,7 +1533,7 @@ async function validateVault(vaultPath) {
1363
1533
  return { ok: false, error: `Path does not exist: ${vaultPath}` };
1364
1534
  }
1365
1535
  try {
1366
- await access3(join13(vaultPath, ".obsidian"));
1536
+ await access4(join14(vaultPath, ".obsidian"));
1367
1537
  } catch {
1368
1538
  return {
1369
1539
  ok: false,
@@ -1383,7 +1553,7 @@ async function countMarkdown(root) {
1383
1553
  }
1384
1554
  for (const e of entries) {
1385
1555
  if (e.name.startsWith(".")) continue;
1386
- const full = join13(dir, e.name);
1556
+ const full = join14(dir, e.name);
1387
1557
  if (e.isDirectory()) await walk(full);
1388
1558
  else if (e.name.endsWith(".md")) count++;
1389
1559
  }
@@ -1392,7 +1562,7 @@ async function countMarkdown(root) {
1392
1562
  return count;
1393
1563
  }
1394
1564
  async function runInit(opts, deps) {
1395
- const rf = deps.readFile ?? readFile8;
1565
+ const rf = deps.readFile ?? readFile9;
1396
1566
  let vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT;
1397
1567
  if (!vaultPath) {
1398
1568
  const registryPath = obsidianRegistryPath(deps.platform, {
@@ -1521,12 +1691,12 @@ async function runInit(opts, deps) {
1521
1691
  }
1522
1692
  const merged = mergeSeekstoneConfig(existing, vaultPath);
1523
1693
  let backupPath;
1524
- await mkdir3(dirname3(configPath), { recursive: true });
1694
+ await mkdir4(dirname4(configPath), { recursive: true });
1525
1695
  if (existingRaw !== null) {
1526
1696
  backupPath = `${configPath}.backup-${deps.timestamp}`;
1527
1697
  await copyFile(configPath, backupPath);
1528
1698
  }
1529
- await writeFile6(configPath, `${JSON.stringify(merged, null, 2)}
1699
+ await writeFile7(configPath, `${JSON.stringify(merged, null, 2)}
1530
1700
  `, "utf8");
1531
1701
  out.push(
1532
1702
  `\u2713 Patched ${configPath}`,
@@ -1598,7 +1768,7 @@ function createLogger(opts = {}) {
1598
1768
  );
1599
1769
  }
1600
1770
  }
1601
- function writeFile7(record) {
1771
+ function writeFile8(record) {
1602
1772
  if (!fileOk || filePath === void 0) return;
1603
1773
  const line = `${JSON.stringify(record)}
1604
1774
  `;
@@ -1620,7 +1790,7 @@ function createLogger(opts = {}) {
1620
1790
  const f = fields ?? {};
1621
1791
  stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
1622
1792
  `);
1623
- writeFile7({ t: ts, level: lvl, msg, ...f });
1793
+ writeFile8({ t: ts, level: lvl, msg, ...f });
1624
1794
  }
1625
1795
  return {
1626
1796
  level,
@@ -1632,8 +1802,8 @@ function createLogger(opts = {}) {
1632
1802
  }
1633
1803
 
1634
1804
  // src/watcher.ts
1635
- import { readFile as readFile9 } from "fs/promises";
1636
- import { join as join14, 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";
1637
1807
  import chokidar from "chokidar";
1638
1808
  function removeNoteBacklinks(ctx2, relPath) {
1639
1809
  const oldDoc = ctx2.notes.get(relPath);
@@ -1670,7 +1840,7 @@ function startWatcher(ctx2, log2, opts) {
1670
1840
  const toRel = (abs) => relative2(ctx2.vaultRoot, abs).split(sep2).join("/");
1671
1841
  async function upsert(relPath, op) {
1672
1842
  try {
1673
- const raw = await readFile9(join14(ctx2.vaultRoot, relPath), "utf8");
1843
+ const raw = await readFile10(join15(ctx2.vaultRoot, relPath), "utf8");
1674
1844
  removeNoteBacklinks(ctx2, relPath);
1675
1845
  upsertDoc(ctx2, buildDoc(relPath, raw));
1676
1846
  addNoteBacklinks(ctx2, relPath, raw);
@@ -1688,6 +1858,7 @@ function startWatcher(ctx2, log2, opts) {
1688
1858
  }
1689
1859
  const onUpsert = (op) => (abs) => {
1690
1860
  const relPath = toRel(abs);
1861
+ opts?.onRawEvent?.(op, abs, relPath);
1691
1862
  if (relPath.endsWith(".md")) void upsert(relPath, op);
1692
1863
  };
1693
1864
  const watcher2 = chokidar.watch(ctx2.vaultRoot, {
@@ -1706,6 +1877,7 @@ function startWatcher(ctx2, log2, opts) {
1706
1877
  });
1707
1878
  watcher2.on("add", onUpsert("add")).on("change", onUpsert("change")).on("unlink", (abs) => {
1708
1879
  const relPath = toRel(abs);
1880
+ opts?.onRawEvent?.("unlink", abs, relPath);
1709
1881
  if (relPath.endsWith(".md")) remove(relPath);
1710
1882
  });
1711
1883
  const ready = new Promise((resolve) => {
@@ -1718,7 +1890,7 @@ function startWatcher(ctx2, log2, opts) {
1718
1890
  }
1719
1891
 
1720
1892
  // src/index.ts
1721
- var VERSION = true ? "0.5.0" : "0.0.0-dev";
1893
+ var VERSION = true ? "0.6.0" : "0.0.0-dev";
1722
1894
  var intent = parseCliIntent(process.argv.slice(2));
1723
1895
  if (intent === "version") {
1724
1896
  process.stdout.write(`${VERSION}
@@ -2031,6 +2203,56 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2031
2203
  },
2032
2204
  required: ["path", "find", "replace"]
2033
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
+ }
2034
2256
  }
2035
2257
  ]
2036
2258
  }));
@@ -2040,7 +2262,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
2040
2262
  });
2041
2263
  var transport = new StdioServerTransport();
2042
2264
  await server.connect(transport);
2043
- log.info("ready", { tools: 14, transport: "stdio" });
2265
+ log.info("ready", { tools: 16, transport: "stdio" });
2044
2266
  process.stderr.write(
2045
2267
  `seekstone: add to Claude Desktop:
2046
2268
  ${JSON.stringify(