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 +393 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -746,30 +746,181 @@ function updateCache(ctx2, path2, newContent, bodyStart) {
|
|
|
746
746
|
}
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
-
// src/tools/
|
|
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
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
880
|
-
query:
|
|
881
|
-
limit:
|
|
882
|
-
folder:
|
|
883
|
-
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
|
|
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
|
|
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
|
|
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
|
|
1180
|
-
import path, { dirname as
|
|
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
|
|
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 =
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1538
|
-
import { join as
|
|
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
|
|
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.
|
|
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:
|
|
2265
|
+
log.info("ready", { tools: 16, transport: "stdio" });
|
|
1910
2266
|
process.stderr.write(
|
|
1911
2267
|
`seekstone: add to Claude Desktop:
|
|
1912
2268
|
${JSON.stringify(
|