seekstone 0.5.0 → 0.6.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/dist/index.js CHANGED
@@ -155,12 +155,12 @@ import { z as z2 } from "zod";
155
155
  import { basename, extname } from "path";
156
156
 
157
157
  // ../core/src/extract.ts
158
- var LINK_WITH_LINES_RE_SRC = /(!?\[\[)([^\]|#\n]+)(#[^\]|\n]+)?(\|[^\]\n]+)?(\]\])/.source;
158
+ var LINK_WITH_LINES_RE = /(!?\[\[)([^\]|#\n]{1,512})(#[^\]|\n]{1,512})?(\|[^\]\n]{1,512})?(\]\])/g;
159
159
  function extractLinksWithLines(raw) {
160
160
  const out = [];
161
161
  const lines = raw.split("\n");
162
162
  for (const [lineIdx, lineText] of lines.entries()) {
163
- for (const m of lineText.matchAll(new RegExp(LINK_WITH_LINES_RE_SRC, "g"))) {
163
+ for (const m of lineText.matchAll(LINK_WITH_LINES_RE)) {
164
164
  const prefix = m[1] ?? "";
165
165
  out.push({
166
166
  raw: m[0] ?? "",
@@ -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).max(1e3).describe("Text or pattern to find (max 1000 chars; bounds regex-mode pattern size)."),
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, readdir, readFile as readFile9, 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}`,
@@ -1571,11 +1741,10 @@ function createLogger(opts = {}) {
1571
1741
  const env = opts.env ?? process.env;
1572
1742
  const stderr = opts.stderr ?? ((line) => void process.stderr.write(line));
1573
1743
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
1574
- const level = parseLevel(
1575
- env.SEEKSTONE_LOG_LEVEL,
1576
- (m) => stderr(`WARN ${now().toISOString()} ${m}
1577
- `)
1578
- );
1744
+ const level = parseLevel(env.SEEKSTONE_LOG_LEVEL, (m) => {
1745
+ stderr(`WARN ${now().toISOString()} ${m}
1746
+ `);
1747
+ });
1579
1748
  const threshold = ORDER.indexOf(level);
1580
1749
  const filePath = env.SEEKSTONE_LOG_FILE;
1581
1750
  const maxSize = parseSize(env.SEEKSTONE_LOG_MAX_SIZE);
@@ -1598,7 +1767,7 @@ function createLogger(opts = {}) {
1598
1767
  );
1599
1768
  }
1600
1769
  }
1601
- function writeFile7(record) {
1770
+ function writeFile8(record) {
1602
1771
  if (!fileOk || filePath === void 0) return;
1603
1772
  const line = `${JSON.stringify(record)}
1604
1773
  `;
@@ -1620,20 +1789,41 @@ function createLogger(opts = {}) {
1620
1789
  const f = fields ?? {};
1621
1790
  stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
1622
1791
  `);
1623
- writeFile7({ t: ts, level: lvl, msg, ...f });
1792
+ writeFile8({ t: ts, level: lvl, msg, ...f });
1624
1793
  }
1625
1794
  return {
1626
1795
  level,
1627
- error: (m, f) => emit("error", m, f),
1628
- warn: (m, f) => emit("warn", m, f),
1629
- info: (m, f) => emit("info", m, f),
1630
- debug: (m, f) => emit("debug", m, f)
1796
+ error: (m, f) => {
1797
+ emit("error", m, f);
1798
+ },
1799
+ warn: (m, f) => {
1800
+ emit("warn", m, f);
1801
+ },
1802
+ info: (m, f) => {
1803
+ emit("info", m, f);
1804
+ },
1805
+ debug: (m, f) => {
1806
+ emit("debug", m, f);
1807
+ }
1808
+ };
1809
+ }
1810
+
1811
+ // src/process-guards.ts
1812
+ function installProcessGuards(log2) {
1813
+ const onUnhandledRejection = (reason) => {
1814
+ const error = reason instanceof Error ? reason.message : String(reason);
1815
+ log2.error("unhandled promise rejection (server kept alive)", { error });
1816
+ };
1817
+ process.on("unhandledRejection", onUnhandledRejection);
1818
+ return () => {
1819
+ process.off("unhandledRejection", onUnhandledRejection);
1631
1820
  };
1632
1821
  }
1633
1822
 
1634
1823
  // src/watcher.ts
1635
- import { readFile as readFile9 } from "fs/promises";
1636
- import { join as join14, relative as relative2, sep as sep2 } from "path";
1824
+ import { realpathSync } from "fs";
1825
+ import { readFile as readFile10 } from "fs/promises";
1826
+ import { join as join15, relative as relative2, sep as sep2 } from "path";
1637
1827
  import chokidar from "chokidar";
1638
1828
  function removeNoteBacklinks(ctx2, relPath) {
1639
1829
  const oldDoc = ctx2.notes.get(relPath);
@@ -1667,10 +1857,15 @@ function addNoteBacklinks(ctx2, relPath, raw) {
1667
1857
  }
1668
1858
  function startWatcher(ctx2, log2, opts) {
1669
1859
  const usePolling = opts?.usePolling ?? process.env.SEEKSTONE_WATCH_POLL === "1";
1670
- const toRel = (abs) => relative2(ctx2.vaultRoot, abs).split(sep2).join("/");
1860
+ let vaultRoot2 = ctx2.vaultRoot;
1861
+ try {
1862
+ vaultRoot2 = realpathSync(ctx2.vaultRoot);
1863
+ } catch {
1864
+ }
1865
+ const toRel = (abs) => relative2(vaultRoot2, abs).split(sep2).join("/");
1671
1866
  async function upsert(relPath, op) {
1672
1867
  try {
1673
- const raw = await readFile9(join14(ctx2.vaultRoot, relPath), "utf8");
1868
+ const raw = await readFile10(join15(vaultRoot2, relPath), "utf8");
1674
1869
  removeNoteBacklinks(ctx2, relPath);
1675
1870
  upsertDoc(ctx2, buildDoc(relPath, raw));
1676
1871
  addNoteBacklinks(ctx2, relPath, raw);
@@ -1688,9 +1883,10 @@ function startWatcher(ctx2, log2, opts) {
1688
1883
  }
1689
1884
  const onUpsert = (op) => (abs) => {
1690
1885
  const relPath = toRel(abs);
1886
+ opts?.onRawEvent?.(op, abs, relPath);
1691
1887
  if (relPath.endsWith(".md")) void upsert(relPath, op);
1692
1888
  };
1693
- const watcher2 = chokidar.watch(ctx2.vaultRoot, {
1889
+ const watcher2 = chokidar.watch(vaultRoot2, {
1694
1890
  // The startup index build already covers existing files.
1695
1891
  ignoreInitial: true,
1696
1892
  usePolling,
@@ -1699,17 +1895,20 @@ function startWatcher(ctx2, log2, opts) {
1699
1895
  // Skip dot-dirs (.obsidian, .git, .trash, …) regardless of where the vault
1700
1896
  // lives — matched on the path relative to the vault root.
1701
1897
  ignored: (p) => {
1702
- const rel = relative2(ctx2.vaultRoot, p);
1898
+ const rel = relative2(vaultRoot2, p);
1703
1899
  if (!rel || rel === ".") return false;
1704
- return rel.split(sep2).filter(Boolean).some((seg) => seg.startsWith("."));
1900
+ return rel.split(sep2).filter(Boolean).some((seg) => seg.startsWith(".") && seg !== "..");
1705
1901
  }
1706
1902
  });
1707
1903
  watcher2.on("add", onUpsert("add")).on("change", onUpsert("change")).on("unlink", (abs) => {
1708
1904
  const relPath = toRel(abs);
1905
+ opts?.onRawEvent?.("unlink", abs, relPath);
1709
1906
  if (relPath.endsWith(".md")) remove(relPath);
1710
1907
  });
1711
1908
  const ready = new Promise((resolve) => {
1712
- watcher2.once("ready", () => resolve());
1909
+ watcher2.once("ready", () => {
1910
+ resolve();
1911
+ });
1713
1912
  });
1714
1913
  return {
1715
1914
  stop: () => void watcher2.close(),
@@ -1718,7 +1917,7 @@ function startWatcher(ctx2, log2, opts) {
1718
1917
  }
1719
1918
 
1720
1919
  // src/index.ts
1721
- var VERSION = true ? "0.5.0" : "0.0.0-dev";
1920
+ var VERSION = true ? "0.6.1" : "0.0.0-dev";
1722
1921
  var intent = parseCliIntent(process.argv.slice(2));
1723
1922
  if (intent === "version") {
1724
1923
  process.stdout.write(`${VERSION}
@@ -1741,6 +1940,7 @@ if (intent === "init") {
1741
1940
  process.exit(result.exitCode);
1742
1941
  }
1743
1942
  var log = createLogger();
1943
+ installProcessGuards(log);
1744
1944
  var vaultRoot = process.env.SEEKSTONE_VAULT;
1745
1945
  if (!vaultRoot) {
1746
1946
  log.error("SEEKSTONE_VAULT env var is required");
@@ -2031,6 +2231,56 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2031
2231
  },
2032
2232
  required: ["path", "find", "replace"]
2033
2233
  }
2234
+ },
2235
+ {
2236
+ name: "get_periodic_note",
2237
+ 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.",
2238
+ inputSchema: {
2239
+ type: "object",
2240
+ properties: {
2241
+ period: {
2242
+ type: "string",
2243
+ enum: ["daily", "weekly", "monthly", "quarterly", "yearly"],
2244
+ description: "Period type. Default: daily."
2245
+ },
2246
+ date: {
2247
+ type: "string",
2248
+ description: "ISO date string (YYYY-MM-DD). Defaults to today when omitted."
2249
+ },
2250
+ createIfMissing: {
2251
+ type: "boolean",
2252
+ description: "Create the note from the configured template if it does not exist. Default false."
2253
+ }
2254
+ },
2255
+ required: []
2256
+ }
2257
+ },
2258
+ {
2259
+ name: "append_periodic_note",
2260
+ 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).",
2261
+ inputSchema: {
2262
+ type: "object",
2263
+ properties: {
2264
+ period: {
2265
+ type: "string",
2266
+ enum: ["daily", "weekly", "monthly", "quarterly", "yearly"],
2267
+ description: "Period type. Default: daily."
2268
+ },
2269
+ date: {
2270
+ type: "string",
2271
+ description: "ISO date string (YYYY-MM-DD). Defaults to today when omitted."
2272
+ },
2273
+ content: {
2274
+ type: "string",
2275
+ description: "Text to append to the note body."
2276
+ },
2277
+ createIfMissing: {
2278
+ type: "boolean",
2279
+ description: "Create the note if it does not exist before appending. Default true."
2280
+ }
2281
+ },
2282
+ required: ["content"]
2283
+ }
2034
2284
  }
2035
2285
  ]
2036
2286
  }));
@@ -2040,7 +2290,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
2040
2290
  });
2041
2291
  var transport = new StdioServerTransport();
2042
2292
  await server.connect(transport);
2043
- log.info("ready", { tools: 14, transport: "stdio" });
2293
+ log.info("ready", { tools: 16, transport: "stdio" });
2044
2294
  process.stderr.write(
2045
2295
  `seekstone: add to Claude Desktop:
2046
2296
  ${JSON.stringify(