seekstone 0.10.0 → 0.11.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 +315 -142
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -74,7 +74,7 @@ Docs: https://github.com/shaqmughal/seekstone`;
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// src/tools/append_note.ts
|
|
77
|
-
import { readFile
|
|
77
|
+
import { readFile } from "fs/promises";
|
|
78
78
|
import { join } from "path";
|
|
79
79
|
|
|
80
80
|
// ../core/src/frontmatter.ts
|
|
@@ -143,6 +143,34 @@ function parseFrontmatter(text) {
|
|
|
143
143
|
// src/tools/append_note.ts
|
|
144
144
|
import { z } from "zod";
|
|
145
145
|
|
|
146
|
+
// src/atomic-write.ts
|
|
147
|
+
import { rename, writeFile } from "fs/promises";
|
|
148
|
+
async function atomicWrite(absPath, content) {
|
|
149
|
+
const tmpPath = `${absPath}.seekstone-tmp`;
|
|
150
|
+
await writeFile(tmpPath, content, "utf8");
|
|
151
|
+
await rename(tmpPath, absPath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/content-hash.ts
|
|
155
|
+
import { createHash } from "crypto";
|
|
156
|
+
function contentHash(raw) {
|
|
157
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
158
|
+
}
|
|
159
|
+
function assertHashMatch(raw, prevHash, path2) {
|
|
160
|
+
const actual = contentHash(raw);
|
|
161
|
+
if (actual !== prevHash) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
error: "hash_conflict",
|
|
165
|
+
path: path2,
|
|
166
|
+
expected: prevHash,
|
|
167
|
+
actual,
|
|
168
|
+
hint: "Note changed on disk since it was read. Re-read it and retry with the new contentHash."
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
146
174
|
// src/policy.ts
|
|
147
175
|
import picomatch from "picomatch";
|
|
148
176
|
function parseWritePolicy(env) {
|
|
@@ -177,6 +205,9 @@ var AppendNoteInput = z.object({
|
|
|
177
205
|
path: z.string().describe("Vault-relative path to the note."),
|
|
178
206
|
content: z.string().min(1).describe(
|
|
179
207
|
"Text to append to the note body. Will be separated from existing content by a blank line."
|
|
208
|
+
),
|
|
209
|
+
prevHash: z.string().optional().describe(
|
|
210
|
+
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
180
211
|
)
|
|
181
212
|
});
|
|
182
213
|
async function appendNote(ctx2, input) {
|
|
@@ -186,6 +217,7 @@ async function appendNote(ctx2, input) {
|
|
|
186
217
|
}
|
|
187
218
|
assertWritable(ctx2.policy, input.path);
|
|
188
219
|
const original = await readFile(absPath, "utf8");
|
|
220
|
+
if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
|
|
189
221
|
const fm = parseFrontmatter(original);
|
|
190
222
|
const body = fm.body.endsWith("\n") ? fm.body : `${fm.body}
|
|
191
223
|
`;
|
|
@@ -193,17 +225,21 @@ async function appendNote(ctx2, input) {
|
|
|
193
225
|
const newBody = `${body}${separator}${input.content}`;
|
|
194
226
|
const header = original.slice(0, fm.bodyStart);
|
|
195
227
|
const newContent = `${header}${newBody}`;
|
|
196
|
-
await
|
|
228
|
+
await atomicWrite(absPath, newContent);
|
|
197
229
|
const cached = ctx2.notes.get(input.path);
|
|
198
230
|
if (cached) {
|
|
199
231
|
cached.body = newBody;
|
|
200
232
|
cached.raw = newContent;
|
|
201
233
|
}
|
|
202
|
-
return {
|
|
234
|
+
return {
|
|
235
|
+
path: input.path,
|
|
236
|
+
bytesWritten: Buffer.byteLength(newContent, "utf8"),
|
|
237
|
+
contentHash: contentHash(newContent)
|
|
238
|
+
};
|
|
203
239
|
}
|
|
204
240
|
|
|
205
241
|
// src/tools/create_note.ts
|
|
206
|
-
import { access, mkdir,
|
|
242
|
+
import { access, mkdir, readFile as readFile2 } from "fs/promises";
|
|
207
243
|
import { dirname, join as join2 } from "path";
|
|
208
244
|
import { stringify as stringifyYaml } from "yaml";
|
|
209
245
|
import { z as z2 } from "zod";
|
|
@@ -282,9 +318,17 @@ var CreateNoteInput = z2.object({
|
|
|
282
318
|
),
|
|
283
319
|
content: z2.string().default("").describe("Body content for the note."),
|
|
284
320
|
frontmatter: z2.record(z2.string(), z2.unknown()).optional().describe("Frontmatter key-value pairs to write as a YAML block at the top of the note."),
|
|
285
|
-
overwrite: z2.boolean().default(false).describe(
|
|
321
|
+
overwrite: z2.boolean().default(false).describe(
|
|
322
|
+
"Overwrite an existing note. Defaults to false \u2014 throws if the note already exists."
|
|
323
|
+
),
|
|
324
|
+
prevHash: z2.string().optional().describe(
|
|
325
|
+
"Optional compare-and-swap guard for overwrite: true \u2014 the contentHash of the note being replaced. Fails with hash_conflict if it changed since it was read."
|
|
326
|
+
)
|
|
327
|
+
}).refine((v) => v.prevHash === void 0 || v.overwrite === true, {
|
|
328
|
+
message: "prevHash only makes sense with overwrite: true (a fresh create has no prior version)."
|
|
286
329
|
});
|
|
287
|
-
async function createNote(ctx2,
|
|
330
|
+
async function createNote(ctx2, rawInput) {
|
|
331
|
+
const input = CreateNoteInput.parse(rawInput);
|
|
288
332
|
const absPath = join2(ctx2.vaultRoot, input.path);
|
|
289
333
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
290
334
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
@@ -297,6 +341,22 @@ async function createNote(ctx2, input) {
|
|
|
297
341
|
} catch (err) {
|
|
298
342
|
if (err.code !== "ENOENT") throw err;
|
|
299
343
|
}
|
|
344
|
+
} else if (input.prevHash !== void 0) {
|
|
345
|
+
let existing;
|
|
346
|
+
try {
|
|
347
|
+
existing = await readFile2(absPath, "utf8");
|
|
348
|
+
} catch {
|
|
349
|
+
throw new Error(
|
|
350
|
+
JSON.stringify({
|
|
351
|
+
error: "hash_conflict",
|
|
352
|
+
path: input.path,
|
|
353
|
+
expected: input.prevHash,
|
|
354
|
+
actual: null,
|
|
355
|
+
hint: "The note no longer exists on disk. Re-check before overwriting."
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
assertHashMatch(existing, input.prevHash, input.path);
|
|
300
360
|
}
|
|
301
361
|
let raw = "";
|
|
302
362
|
if (input.frontmatter && Object.keys(input.frontmatter).length > 0) {
|
|
@@ -306,30 +366,112 @@ ${stringifyYaml(input.frontmatter)}---
|
|
|
306
366
|
}
|
|
307
367
|
raw += input.content ?? "";
|
|
308
368
|
await mkdir(dirname(absPath), { recursive: true });
|
|
309
|
-
await
|
|
369
|
+
await atomicWrite(absPath, raw);
|
|
310
370
|
upsertDoc(ctx2, buildDoc(input.path, raw));
|
|
311
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
path: input.path,
|
|
373
|
+
bytesWritten: Buffer.byteLength(raw, "utf8"),
|
|
374
|
+
contentHash: contentHash(raw)
|
|
375
|
+
};
|
|
312
376
|
}
|
|
313
377
|
|
|
314
378
|
// src/tools/delete_note.ts
|
|
315
|
-
import { rm } from "fs/promises";
|
|
316
|
-
import { join as join3 } from "path";
|
|
379
|
+
import { access as access2, mkdir as mkdir2, rename as rename2, rm } from "fs/promises";
|
|
380
|
+
import { basename as basename2, extname as extname2, join as join3 } from "path";
|
|
317
381
|
import { z as z3 } from "zod";
|
|
382
|
+
|
|
383
|
+
// src/index/resolve.ts
|
|
384
|
+
function resolveLink(target, notes2) {
|
|
385
|
+
if (notes2.has(target)) return target;
|
|
386
|
+
const withMd = `${target}.md`;
|
|
387
|
+
if (notes2.has(withMd)) return withMd;
|
|
388
|
+
const targetLower = target.toLowerCase();
|
|
389
|
+
const targetBase = (target.split("/").pop() ?? target).toLowerCase();
|
|
390
|
+
for (const path2 of notes2.keys()) {
|
|
391
|
+
const pathNoExt = path2.replace(/\.md$/i, "");
|
|
392
|
+
if (pathNoExt.toLowerCase() === targetLower) return path2;
|
|
393
|
+
const pathBase = (pathNoExt.split("/").pop() ?? pathNoExt).toLowerCase();
|
|
394
|
+
if (pathBase === targetBase) return path2;
|
|
395
|
+
}
|
|
396
|
+
return void 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/index/backlinks.ts
|
|
400
|
+
function removeNoteBacklinks(ctx2, relPath) {
|
|
401
|
+
const oldDoc = ctx2.notes.get(relPath);
|
|
402
|
+
if (oldDoc === void 0) return;
|
|
403
|
+
for (const link of extractLinksWithLines(oldDoc.raw)) {
|
|
404
|
+
const resolved = resolveLink(link.target, ctx2.notes);
|
|
405
|
+
if (resolved === void 0) continue;
|
|
406
|
+
const arr = ctx2.backlinks.get(resolved);
|
|
407
|
+
if (arr === void 0) continue;
|
|
408
|
+
const filtered = arr.filter((r) => r.path !== relPath);
|
|
409
|
+
ctx2.backlinks.set(resolved, filtered);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function addNoteBacklinks(ctx2, relPath, raw) {
|
|
413
|
+
const seen = /* @__PURE__ */ new Set();
|
|
414
|
+
for (const link of extractLinksWithLines(raw)) {
|
|
415
|
+
const resolved = resolveLink(link.target, ctx2.notes);
|
|
416
|
+
if (resolved === void 0) continue;
|
|
417
|
+
const dedupeKey = `${relPath}\0${resolved}`;
|
|
418
|
+
if (seen.has(dedupeKey)) continue;
|
|
419
|
+
seen.add(dedupeKey);
|
|
420
|
+
let arr = ctx2.backlinks.get(resolved);
|
|
421
|
+
if (arr === void 0) {
|
|
422
|
+
arr = [];
|
|
423
|
+
ctx2.backlinks.set(resolved, arr);
|
|
424
|
+
}
|
|
425
|
+
const ref = { path: relPath, line: link.line, linkType: link.linkType };
|
|
426
|
+
arr.push(ref);
|
|
427
|
+
arr.sort((a, b) => a.path.localeCompare(b.path));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/tools/delete_note.ts
|
|
318
432
|
var DeleteNoteInput = z3.object({
|
|
319
|
-
path: z3.string().describe("Vault-relative path of the note to delete.")
|
|
433
|
+
path: z3.string().describe("Vault-relative path of the note to delete."),
|
|
434
|
+
permanent: z3.boolean().default(false).describe(
|
|
435
|
+
"Permanently remove the note instead of moving it to the vault .trash/ folder. Defaults to false \u2014 deletes are recoverable."
|
|
436
|
+
)
|
|
320
437
|
});
|
|
321
|
-
async function
|
|
438
|
+
async function freeTrashPath(vaultRoot2, noteBase) {
|
|
439
|
+
const ext = extname2(noteBase);
|
|
440
|
+
const stem = noteBase.slice(0, noteBase.length - ext.length);
|
|
441
|
+
let candidate = `.trash/${noteBase}`;
|
|
442
|
+
for (; ; ) {
|
|
443
|
+
try {
|
|
444
|
+
await access2(join3(vaultRoot2, candidate));
|
|
445
|
+
} catch {
|
|
446
|
+
return candidate;
|
|
447
|
+
}
|
|
448
|
+
candidate = `.trash/${stem}.${Date.now()}${ext}`;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function deleteNote(ctx2, rawInput) {
|
|
452
|
+
const input = DeleteNoteInput.parse(rawInput);
|
|
322
453
|
const absPath = join3(ctx2.vaultRoot, input.path);
|
|
323
454
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
324
455
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
325
456
|
}
|
|
326
457
|
assertWritable(ctx2.policy, input.path);
|
|
327
|
-
|
|
458
|
+
let trashedTo;
|
|
459
|
+
if (input.permanent) {
|
|
460
|
+
await rm(absPath);
|
|
461
|
+
} else {
|
|
462
|
+
await access2(absPath);
|
|
463
|
+
await mkdir2(join3(ctx2.vaultRoot, ".trash"), { recursive: true });
|
|
464
|
+
trashedTo = await freeTrashPath(ctx2.vaultRoot, basename2(input.path));
|
|
465
|
+
await rename2(absPath, join3(ctx2.vaultRoot, trashedTo));
|
|
466
|
+
}
|
|
467
|
+
removeNoteBacklinks(ctx2, input.path);
|
|
328
468
|
if (ctx2.notes.has(input.path)) {
|
|
329
469
|
ctx2.index.discard(input.path);
|
|
330
470
|
ctx2.notes.delete(input.path);
|
|
331
471
|
}
|
|
332
|
-
|
|
472
|
+
const result = { path: input.path, permanent: input.permanent };
|
|
473
|
+
if (trashedTo !== void 0) result.trashedTo = trashedTo;
|
|
474
|
+
return result;
|
|
333
475
|
}
|
|
334
476
|
|
|
335
477
|
// src/tools/get_backlinks.ts
|
|
@@ -372,24 +514,6 @@ function getBacklinks(ctx2, input) {
|
|
|
372
514
|
// src/tools/get_links.ts
|
|
373
515
|
import { join as join5 } from "path";
|
|
374
516
|
import { z as z5 } from "zod";
|
|
375
|
-
|
|
376
|
-
// src/index/resolve.ts
|
|
377
|
-
function resolveLink(target, notes2) {
|
|
378
|
-
if (notes2.has(target)) return target;
|
|
379
|
-
const withMd = `${target}.md`;
|
|
380
|
-
if (notes2.has(withMd)) return withMd;
|
|
381
|
-
const targetLower = target.toLowerCase();
|
|
382
|
-
const targetBase = (target.split("/").pop() ?? target).toLowerCase();
|
|
383
|
-
for (const path2 of notes2.keys()) {
|
|
384
|
-
const pathNoExt = path2.replace(/\.md$/i, "");
|
|
385
|
-
if (pathNoExt.toLowerCase() === targetLower) return path2;
|
|
386
|
-
const pathBase = (pathNoExt.split("/").pop() ?? pathNoExt).toLowerCase();
|
|
387
|
-
if (pathBase === targetBase) return path2;
|
|
388
|
-
}
|
|
389
|
-
return void 0;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// src/tools/get_links.ts
|
|
393
517
|
var GetLinksInput = z5.object({
|
|
394
518
|
path: z5.string().describe("Vault-relative path to the note.")
|
|
395
519
|
});
|
|
@@ -481,42 +605,10 @@ function listTags(ctx2, input) {
|
|
|
481
605
|
}
|
|
482
606
|
|
|
483
607
|
// src/tools/move_note.ts
|
|
484
|
-
import { access as
|
|
608
|
+
import { access as access3, mkdir as mkdir3, rename as rename3 } from "fs/promises";
|
|
485
609
|
import { dirname as dirname2, join as join6 } from "path";
|
|
486
610
|
import { z as z8 } from "zod";
|
|
487
611
|
|
|
488
|
-
// src/index/backlinks.ts
|
|
489
|
-
function removeNoteBacklinks(ctx2, relPath) {
|
|
490
|
-
const oldDoc = ctx2.notes.get(relPath);
|
|
491
|
-
if (oldDoc === void 0) return;
|
|
492
|
-
for (const link of extractLinksWithLines(oldDoc.raw)) {
|
|
493
|
-
const resolved = resolveLink(link.target, ctx2.notes);
|
|
494
|
-
if (resolved === void 0) continue;
|
|
495
|
-
const arr = ctx2.backlinks.get(resolved);
|
|
496
|
-
if (arr === void 0) continue;
|
|
497
|
-
const filtered = arr.filter((r) => r.path !== relPath);
|
|
498
|
-
ctx2.backlinks.set(resolved, filtered);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
function addNoteBacklinks(ctx2, relPath, raw) {
|
|
502
|
-
const seen = /* @__PURE__ */ new Set();
|
|
503
|
-
for (const link of extractLinksWithLines(raw)) {
|
|
504
|
-
const resolved = resolveLink(link.target, ctx2.notes);
|
|
505
|
-
if (resolved === void 0) continue;
|
|
506
|
-
const dedupeKey = `${relPath}\0${resolved}`;
|
|
507
|
-
if (seen.has(dedupeKey)) continue;
|
|
508
|
-
seen.add(dedupeKey);
|
|
509
|
-
let arr = ctx2.backlinks.get(resolved);
|
|
510
|
-
if (arr === void 0) {
|
|
511
|
-
arr = [];
|
|
512
|
-
ctx2.backlinks.set(resolved, arr);
|
|
513
|
-
}
|
|
514
|
-
const ref = { path: relPath, line: link.line, linkType: link.linkType };
|
|
515
|
-
arr.push(ref);
|
|
516
|
-
arr.sort((a, b) => a.path.localeCompare(b.path));
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
612
|
// src/tools/rewrite_links.ts
|
|
521
613
|
import { posix } from "path";
|
|
522
614
|
var WIKI_RE = /(!?)\[\[([^\]|#\n]{1,512})(#[^\]|\n]{1,512})?(\|[^\]\n]{1,512})?\]\]/g;
|
|
@@ -625,13 +717,13 @@ async function moveNote(ctx2, rawInput) {
|
|
|
625
717
|
assertWritable(ctx2.policy, input.from);
|
|
626
718
|
assertWritable(ctx2.policy, input.to);
|
|
627
719
|
try {
|
|
628
|
-
await
|
|
720
|
+
await access3(absFrom);
|
|
629
721
|
} catch {
|
|
630
722
|
throw new Error(`Note not found: ${input.from}`);
|
|
631
723
|
}
|
|
632
724
|
if (!input.overwrite) {
|
|
633
725
|
try {
|
|
634
|
-
await
|
|
726
|
+
await access3(absTo);
|
|
635
727
|
throw new Error(
|
|
636
728
|
`Destination already exists: ${input.to}. Pass overwrite: true to replace it.`
|
|
637
729
|
);
|
|
@@ -649,8 +741,8 @@ async function moveNote(ctx2, rawInput) {
|
|
|
649
741
|
}
|
|
650
742
|
candidates.delete(input.from);
|
|
651
743
|
}
|
|
652
|
-
await
|
|
653
|
-
await
|
|
744
|
+
await mkdir3(dirname2(absTo), { recursive: true });
|
|
745
|
+
await rename3(absFrom, absTo);
|
|
654
746
|
const raw = ctx2.notes.get(input.from)?.raw ?? "";
|
|
655
747
|
removeNoteBacklinks(ctx2, input.from);
|
|
656
748
|
if (ctx2.notes.has(input.from)) {
|
|
@@ -679,7 +771,7 @@ async function moveNote(ctx2, rawInput) {
|
|
|
679
771
|
);
|
|
680
772
|
removeNoteBacklinks(ctx2, path2);
|
|
681
773
|
if (count > 0) {
|
|
682
|
-
await
|
|
774
|
+
await atomicWrite(join6(ctx2.vaultRoot, path2), content);
|
|
683
775
|
upsertDoc(ctx2, buildDoc(path2, content));
|
|
684
776
|
notesRewritten++;
|
|
685
777
|
linksRewritten += count;
|
|
@@ -695,7 +787,7 @@ async function moveNote(ctx2, rawInput) {
|
|
|
695
787
|
}
|
|
696
788
|
|
|
697
789
|
// src/tools/outline_note.ts
|
|
698
|
-
import { readFile as
|
|
790
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
699
791
|
import { join as join7 } from "path";
|
|
700
792
|
|
|
701
793
|
// ../core/src/outline.ts
|
|
@@ -757,7 +849,7 @@ async function outlineNote(ctx2, input) {
|
|
|
757
849
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
758
850
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
759
851
|
}
|
|
760
|
-
const raw = await
|
|
852
|
+
const raw = await readFile3(absPath, "utf8");
|
|
761
853
|
return buildOutline(raw, {
|
|
762
854
|
includeBlocks: input.includeBlocks,
|
|
763
855
|
includeSizes: input.includeSizes
|
|
@@ -765,7 +857,7 @@ async function outlineNote(ctx2, input) {
|
|
|
765
857
|
}
|
|
766
858
|
|
|
767
859
|
// src/tools/patch_frontmatter.ts
|
|
768
|
-
import { readFile as
|
|
860
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
769
861
|
import { join as join8 } from "path";
|
|
770
862
|
import { parseDocument } from "yaml";
|
|
771
863
|
import { z as z10 } from "zod";
|
|
@@ -773,6 +865,9 @@ var PatchFrontmatterInput = z10.object({
|
|
|
773
865
|
path: z10.string().describe("Vault-relative path to the note."),
|
|
774
866
|
patch: z10.record(z10.string(), z10.unknown()).describe(
|
|
775
867
|
"Key-value pairs to set in the frontmatter. Existing keys not in the patch are unchanged. Pass null as a value to remove a key."
|
|
868
|
+
),
|
|
869
|
+
prevHash: z10.string().optional().describe(
|
|
870
|
+
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
776
871
|
)
|
|
777
872
|
});
|
|
778
873
|
async function patchFrontmatter(ctx2, input) {
|
|
@@ -781,7 +876,8 @@ async function patchFrontmatter(ctx2, input) {
|
|
|
781
876
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
782
877
|
}
|
|
783
878
|
assertWritable(ctx2.policy, input.path);
|
|
784
|
-
const original = await
|
|
879
|
+
const original = await readFile4(absPath, "utf8");
|
|
880
|
+
if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
|
|
785
881
|
const fm = parseFrontmatter(original);
|
|
786
882
|
const keysChanged = [];
|
|
787
883
|
const keysAdded = [];
|
|
@@ -823,18 +919,24 @@ ${original}`;
|
|
|
823
919
|
const tail = opensWithCRLF ? "---\r\n" : "---\n";
|
|
824
920
|
newContent = `${head}${doc.toString()}${tail}${fm.body}`;
|
|
825
921
|
}
|
|
826
|
-
await
|
|
922
|
+
await atomicWrite(absPath, newContent);
|
|
827
923
|
const cached = ctx2.notes.get(input.path);
|
|
828
924
|
if (cached) {
|
|
829
925
|
const updated = parseFrontmatter(newContent);
|
|
830
926
|
cached.raw = newContent;
|
|
831
927
|
cached.fmKeys = updated.keys.join(" ");
|
|
832
928
|
}
|
|
833
|
-
return {
|
|
929
|
+
return {
|
|
930
|
+
path: input.path,
|
|
931
|
+
keysChanged,
|
|
932
|
+
keysAdded,
|
|
933
|
+
keysRemoved,
|
|
934
|
+
contentHash: contentHash(newContent)
|
|
935
|
+
};
|
|
834
936
|
}
|
|
835
937
|
|
|
836
938
|
// src/tools/patch_note.ts
|
|
837
|
-
import { readFile as
|
|
939
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
838
940
|
import { join as join9 } from "path";
|
|
839
941
|
import { z as z11 } from "zod";
|
|
840
942
|
var PatchNoteInput = z11.object({
|
|
@@ -849,6 +951,9 @@ var PatchNoteInput = z11.object({
|
|
|
849
951
|
content: z11.string().min(1).describe("Content to insert or replace with."),
|
|
850
952
|
createIfMissing: z11.boolean().default(false).describe(
|
|
851
953
|
"If the heading target is not found, append a new heading (level 2) + content. Only valid for heading targets."
|
|
954
|
+
),
|
|
955
|
+
prevHash: z11.string().optional().describe(
|
|
956
|
+
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
852
957
|
)
|
|
853
958
|
});
|
|
854
959
|
function lineEnd(raw, offset) {
|
|
@@ -860,18 +965,14 @@ function lineStart(raw, offset) {
|
|
|
860
965
|
const prev = raw.lastIndexOf("\n", offset - 1);
|
|
861
966
|
return prev === -1 ? 0 : prev + 1;
|
|
862
967
|
}
|
|
863
|
-
async function atomicWrite(absPath, content) {
|
|
864
|
-
const tmpPath = `${absPath}.seekstone-patch-tmp`;
|
|
865
|
-
await writeFile5(tmpPath, content, "utf8");
|
|
866
|
-
await rename2(tmpPath, absPath);
|
|
867
|
-
}
|
|
868
968
|
async function patchNote(ctx2, input) {
|
|
869
969
|
const absPath = join9(ctx2.vaultRoot, input.path);
|
|
870
970
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
871
971
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
872
972
|
}
|
|
873
973
|
assertWritable(ctx2.policy, input.path);
|
|
874
|
-
const raw = await
|
|
974
|
+
const raw = await readFile5(absPath, "utf8");
|
|
975
|
+
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
|
|
875
976
|
const fm = parseFrontmatter(raw);
|
|
876
977
|
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
877
978
|
const outline = buildOutline(raw, { includeBlocks: true });
|
|
@@ -899,7 +1000,8 @@ ${input.content}${contentNl}`;
|
|
|
899
1000
|
line: newContent2.slice(0, headingOffset).split("\n").length,
|
|
900
1001
|
byteOffset: headingOffset
|
|
901
1002
|
},
|
|
902
|
-
frontmatterUnchanged: true
|
|
1003
|
+
frontmatterUnchanged: true,
|
|
1004
|
+
contentHash: contentHash(newContent2)
|
|
903
1005
|
};
|
|
904
1006
|
}
|
|
905
1007
|
throw new Error(
|
|
@@ -964,11 +1066,12 @@ ${input.content}${contentNl}`;
|
|
|
964
1066
|
path: input.path,
|
|
965
1067
|
bytesWritten: Buffer.byteLength(newContent, "utf8"),
|
|
966
1068
|
targetResolvedAt: resolvedAt,
|
|
967
|
-
frontmatterUnchanged: true
|
|
1069
|
+
frontmatterUnchanged: true,
|
|
1070
|
+
contentHash: contentHash(newContent)
|
|
968
1071
|
};
|
|
969
1072
|
}
|
|
970
1073
|
async function verifyFrontmatter(absPath, originalFmRegion) {
|
|
971
|
-
const written = await
|
|
1074
|
+
const written = await readFile5(absPath, "utf8");
|
|
972
1075
|
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
973
1076
|
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
974
1077
|
}
|
|
@@ -983,7 +1086,7 @@ function updateCache(ctx2, path2, newContent, bodyStart) {
|
|
|
983
1086
|
}
|
|
984
1087
|
|
|
985
1088
|
// src/tools/periodic_note.ts
|
|
986
|
-
import { access as
|
|
1089
|
+
import { access as access4, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
|
|
987
1090
|
import { dirname as dirname3, join as join10 } from "path";
|
|
988
1091
|
import { z as z12 } from "zod";
|
|
989
1092
|
var PERIOD = z12.enum(["daily", "weekly", "monthly", "quarterly", "yearly"]);
|
|
@@ -996,7 +1099,7 @@ var DEFAULTS = {
|
|
|
996
1099
|
};
|
|
997
1100
|
async function readJson(absPath) {
|
|
998
1101
|
try {
|
|
999
|
-
return JSON.parse(await
|
|
1102
|
+
return JSON.parse(await readFile6(absPath, "utf8"));
|
|
1000
1103
|
} catch {
|
|
1001
1104
|
return null;
|
|
1002
1105
|
}
|
|
@@ -1069,7 +1172,7 @@ async function getPeriodicNote(ctx2, rawInput) {
|
|
|
1069
1172
|
if (!abs.startsWith(ctx2.vaultRoot)) throw new Error(`Path outside vault: ${path2}`);
|
|
1070
1173
|
let existed = false;
|
|
1071
1174
|
try {
|
|
1072
|
-
await
|
|
1175
|
+
await access4(abs);
|
|
1073
1176
|
existed = true;
|
|
1074
1177
|
} catch {
|
|
1075
1178
|
}
|
|
@@ -1084,21 +1187,24 @@ async function getPeriodicNote(ctx2, rawInput) {
|
|
|
1084
1187
|
);
|
|
1085
1188
|
if (templateAbs.startsWith(ctx2.vaultRoot)) {
|
|
1086
1189
|
try {
|
|
1087
|
-
body = await
|
|
1190
|
+
body = await readFile6(templateAbs, "utf8");
|
|
1088
1191
|
} catch {
|
|
1089
1192
|
}
|
|
1090
1193
|
}
|
|
1091
1194
|
}
|
|
1092
|
-
await
|
|
1093
|
-
await
|
|
1195
|
+
await mkdir4(dirname3(abs), { recursive: true });
|
|
1196
|
+
await atomicWrite(abs, body);
|
|
1094
1197
|
upsertDoc(ctx2, buildDoc(path2, body));
|
|
1095
|
-
return { path: path2, existed: false, created: true };
|
|
1198
|
+
return { path: path2, existed: false, created: true, contentHash: contentHash(body) };
|
|
1096
1199
|
}
|
|
1097
1200
|
var AppendPeriodicNoteInput = z12.object({
|
|
1098
1201
|
period: PERIOD.default("daily").describe("Period type. Default: daily."),
|
|
1099
1202
|
date: z12.string().optional().describe("ISO date string (YYYY-MM-DD). Defaults to today."),
|
|
1100
1203
|
content: z12.string().min(1).describe("Text to append to the note body. Separated from existing content by a blank line."),
|
|
1101
|
-
createIfMissing: z12.boolean().default(true).describe("Create the note if it does not exist before appending. Default true.")
|
|
1204
|
+
createIfMissing: z12.boolean().default(true).describe("Create the note if it does not exist before appending. Default true."),
|
|
1205
|
+
prevHash: z12.string().optional().describe(
|
|
1206
|
+
"Optional compare-and-swap guard: the contentHash from a prior read of the resolved periodic note. Fails with hash_conflict if it changed since."
|
|
1207
|
+
)
|
|
1102
1208
|
});
|
|
1103
1209
|
async function appendPeriodicNote(ctx2, rawInput) {
|
|
1104
1210
|
const input = AppendPeriodicNoteInput.parse(rawInput);
|
|
@@ -1110,14 +1216,30 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1110
1216
|
assertWritable(ctx2.policy, path2);
|
|
1111
1217
|
let original = "";
|
|
1112
1218
|
try {
|
|
1113
|
-
original = await
|
|
1219
|
+
original = await readFile6(abs, "utf8");
|
|
1220
|
+
if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, path2);
|
|
1114
1221
|
} catch (err) {
|
|
1115
1222
|
if (err.code !== "ENOENT") throw err;
|
|
1223
|
+
if (input.prevHash !== void 0) {
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
JSON.stringify({
|
|
1226
|
+
error: "hash_conflict",
|
|
1227
|
+
path: path2,
|
|
1228
|
+
expected: input.prevHash,
|
|
1229
|
+
actual: null,
|
|
1230
|
+
hint: "The periodic note no longer exists on disk. Re-read before retrying."
|
|
1231
|
+
})
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1116
1234
|
if (!input.createIfMissing) throw new Error(`Periodic note not found: ${path2}`);
|
|
1117
|
-
await
|
|
1118
|
-
await
|
|
1235
|
+
await mkdir4(dirname3(abs), { recursive: true });
|
|
1236
|
+
await atomicWrite(abs, input.content);
|
|
1119
1237
|
upsertDoc(ctx2, buildDoc(path2, input.content));
|
|
1120
|
-
return {
|
|
1238
|
+
return {
|
|
1239
|
+
path: path2,
|
|
1240
|
+
bytesWritten: Buffer.byteLength(input.content, "utf8"),
|
|
1241
|
+
contentHash: contentHash(input.content)
|
|
1242
|
+
};
|
|
1121
1243
|
}
|
|
1122
1244
|
const fm = parseFrontmatter(original);
|
|
1123
1245
|
const body = fm.body.endsWith("\n") ? fm.body : `${fm.body}
|
|
@@ -1126,13 +1248,17 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1126
1248
|
const newBody = `${body}${separator}${input.content}`;
|
|
1127
1249
|
const header = original.slice(0, fm.bodyStart);
|
|
1128
1250
|
const newRaw = `${header}${newBody}`;
|
|
1129
|
-
await
|
|
1251
|
+
await atomicWrite(abs, newRaw);
|
|
1130
1252
|
const cached = ctx2.notes.get(path2);
|
|
1131
1253
|
if (cached) {
|
|
1132
1254
|
cached.body = newBody;
|
|
1133
1255
|
cached.raw = newRaw;
|
|
1134
1256
|
}
|
|
1135
|
-
return {
|
|
1257
|
+
return {
|
|
1258
|
+
path: path2,
|
|
1259
|
+
bytesWritten: Buffer.byteLength(newRaw, "utf8"),
|
|
1260
|
+
contentHash: contentHash(newRaw)
|
|
1261
|
+
};
|
|
1136
1262
|
}
|
|
1137
1263
|
|
|
1138
1264
|
// src/tools/query_notes.ts
|
|
@@ -1341,7 +1467,7 @@ function queryNotes(ctx2, input) {
|
|
|
1341
1467
|
}
|
|
1342
1468
|
|
|
1343
1469
|
// src/tools/read_note.ts
|
|
1344
|
-
import { readFile as
|
|
1470
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1345
1471
|
import { join as join11 } from "path";
|
|
1346
1472
|
import { z as z15 } from "zod";
|
|
1347
1473
|
var ReadNoteInput = z15.object({
|
|
@@ -1363,10 +1489,17 @@ async function readNote(ctx2, input) {
|
|
|
1363
1489
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
1364
1490
|
throw new Error(`Path outside vault: ${input.path}`);
|
|
1365
1491
|
}
|
|
1366
|
-
const raw = await
|
|
1492
|
+
const raw = await readFile7(absPath, "utf8");
|
|
1367
1493
|
const noteBytes = Buffer.byteLength(raw, "utf8");
|
|
1494
|
+
const hash = contentHash(raw);
|
|
1368
1495
|
if (input.section === void 0 && input.block === void 0 && input.lines === void 0) {
|
|
1369
|
-
return {
|
|
1496
|
+
return {
|
|
1497
|
+
path: input.path,
|
|
1498
|
+
content: raw,
|
|
1499
|
+
bytesReturned: noteBytes,
|
|
1500
|
+
noteBytes,
|
|
1501
|
+
contentHash: hash
|
|
1502
|
+
};
|
|
1370
1503
|
}
|
|
1371
1504
|
const fm = parseFrontmatter(raw);
|
|
1372
1505
|
const outline = buildOutline(raw, { includeBlocks: true });
|
|
@@ -1438,12 +1571,13 @@ async function readNote(ctx2, input) {
|
|
|
1438
1571
|
content,
|
|
1439
1572
|
bytesReturned: Buffer.byteLength(content, "utf8"),
|
|
1440
1573
|
noteBytes,
|
|
1574
|
+
contentHash: hash,
|
|
1441
1575
|
span: { charStart, charEnd }
|
|
1442
1576
|
};
|
|
1443
1577
|
}
|
|
1444
1578
|
|
|
1445
1579
|
// src/tools/replace_in_note.ts
|
|
1446
|
-
import { readFile as
|
|
1580
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1447
1581
|
import { join as join12 } from "path";
|
|
1448
1582
|
import { z as z16 } from "zod";
|
|
1449
1583
|
var ReplaceInNoteInput = z16.object({
|
|
@@ -1454,7 +1588,10 @@ var ReplaceInNoteInput = z16.object({
|
|
|
1454
1588
|
caseSensitive: z16.boolean().default(false).describe("Case-sensitive matching."),
|
|
1455
1589
|
wholeWord: z16.boolean().default(false).describe("Match whole words only (\\b word-boundary)."),
|
|
1456
1590
|
limit: z16.number().int().min(1).optional().describe("Maximum number of replacements. Omit to replace all."),
|
|
1457
|
-
dryRun: z16.boolean().default(false).describe("If true, report matches without writing anything.")
|
|
1591
|
+
dryRun: z16.boolean().default(false).describe("If true, report matches without writing anything."),
|
|
1592
|
+
prevHash: z16.string().optional().describe(
|
|
1593
|
+
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
1594
|
+
)
|
|
1458
1595
|
});
|
|
1459
1596
|
function escapeRegex(s) {
|
|
1460
1597
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1465,11 +1602,6 @@ function buildRegex(find, opts) {
|
|
|
1465
1602
|
const flags = `g${opts.caseSensitive ? "" : "i"}`;
|
|
1466
1603
|
return new RegExp(pattern, flags);
|
|
1467
1604
|
}
|
|
1468
|
-
async function atomicWrite2(absPath, content) {
|
|
1469
|
-
const tmpPath = `${absPath}.seekstone-replace-tmp`;
|
|
1470
|
-
await writeFile7(tmpPath, content, "utf8");
|
|
1471
|
-
await rename3(tmpPath, absPath);
|
|
1472
|
-
}
|
|
1473
1605
|
async function replaceInNote(ctx2, input) {
|
|
1474
1606
|
const absPath = join12(ctx2.vaultRoot, input.path);
|
|
1475
1607
|
if (!absPath.startsWith(ctx2.vaultRoot)) {
|
|
@@ -1486,7 +1618,8 @@ async function replaceInNote(ctx2, input) {
|
|
|
1486
1618
|
} catch (err) {
|
|
1487
1619
|
throw new Error(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
1488
1620
|
}
|
|
1489
|
-
const raw = await
|
|
1621
|
+
const raw = await readFile8(absPath, "utf8");
|
|
1622
|
+
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
|
|
1490
1623
|
const fm = parseFrontmatter(raw);
|
|
1491
1624
|
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
1492
1625
|
const body = raw.slice(fm.bodyStart);
|
|
@@ -1516,8 +1649,8 @@ async function replaceInNote(ctx2, input) {
|
|
|
1516
1649
|
}
|
|
1517
1650
|
newBody += body.slice(lastIndex);
|
|
1518
1651
|
const newContent = `${originalFmRegion}${newBody}`;
|
|
1519
|
-
await
|
|
1520
|
-
const written = await
|
|
1652
|
+
await atomicWrite(absPath, newContent);
|
|
1653
|
+
const written = await readFile8(absPath, "utf8");
|
|
1521
1654
|
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
1522
1655
|
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
1523
1656
|
}
|
|
@@ -1532,7 +1665,8 @@ async function replaceInNote(ctx2, input) {
|
|
|
1532
1665
|
replacements: toReplace.length,
|
|
1533
1666
|
matches: matches2,
|
|
1534
1667
|
bytesWritten: Buffer.byteLength(newContent, "utf8"),
|
|
1535
|
-
frontmatterUnchanged: true
|
|
1668
|
+
frontmatterUnchanged: true,
|
|
1669
|
+
contentHash: contentHash(newContent)
|
|
1536
1670
|
};
|
|
1537
1671
|
}
|
|
1538
1672
|
|
|
@@ -1565,7 +1699,9 @@ var META_KEYS = [
|
|
|
1565
1699
|
"minSizeBytes",
|
|
1566
1700
|
"maxSizeBytes",
|
|
1567
1701
|
"period",
|
|
1568
|
-
"date"
|
|
1702
|
+
"date",
|
|
1703
|
+
"permanent",
|
|
1704
|
+
"prevHash"
|
|
1569
1705
|
];
|
|
1570
1706
|
function safeMeta(args) {
|
|
1571
1707
|
if (typeof args !== "object" || args === null) return {};
|
|
@@ -1653,13 +1789,19 @@ async function run(ctx2, name, args) {
|
|
|
1653
1789
|
const input = CreateNoteInput.parse(args);
|
|
1654
1790
|
const result = await createNote(ctx2, input);
|
|
1655
1791
|
return {
|
|
1656
|
-
content: [
|
|
1792
|
+
content: [
|
|
1793
|
+
{
|
|
1794
|
+
type: "text",
|
|
1795
|
+
text: `Created ${result.path} (${result.bytesWritten} bytes). contentHash: ${result.contentHash}`
|
|
1796
|
+
}
|
|
1797
|
+
]
|
|
1657
1798
|
};
|
|
1658
1799
|
}
|
|
1659
1800
|
case "delete_note": {
|
|
1660
1801
|
const input = DeleteNoteInput.parse(args);
|
|
1661
|
-
await deleteNote(ctx2, input);
|
|
1662
|
-
|
|
1802
|
+
const result = await deleteNote(ctx2, input);
|
|
1803
|
+
const text = result.trashedTo ? `Moved ${result.path} to ${result.trashedTo} (recoverable \u2014 restore by moving it back).` : `Permanently deleted ${result.path}.`;
|
|
1804
|
+
return { content: [{ type: "text", text }] };
|
|
1663
1805
|
}
|
|
1664
1806
|
case "move_note": {
|
|
1665
1807
|
const input = MoveNoteInput.parse(args);
|
|
@@ -1675,7 +1817,10 @@ async function run(ctx2, name, args) {
|
|
|
1675
1817
|
const result = await appendNote(ctx2, input);
|
|
1676
1818
|
return {
|
|
1677
1819
|
content: [
|
|
1678
|
-
{
|
|
1820
|
+
{
|
|
1821
|
+
type: "text",
|
|
1822
|
+
text: `Appended ${result.bytesWritten} bytes to ${result.path}. contentHash: ${result.contentHash}`
|
|
1823
|
+
}
|
|
1679
1824
|
]
|
|
1680
1825
|
};
|
|
1681
1826
|
}
|
|
@@ -1721,7 +1866,7 @@ async function run(ctx2, name, args) {
|
|
|
1721
1866
|
content: [
|
|
1722
1867
|
{
|
|
1723
1868
|
type: "text",
|
|
1724
|
-
text: `Appended ${result.bytesWritten} bytes to ${result.path}
|
|
1869
|
+
text: `Appended ${result.bytesWritten} bytes to ${result.path}. contentHash: ${result.contentHash}`
|
|
1725
1870
|
}
|
|
1726
1871
|
]
|
|
1727
1872
|
};
|
|
@@ -1732,12 +1877,12 @@ async function run(ctx2, name, args) {
|
|
|
1732
1877
|
}
|
|
1733
1878
|
|
|
1734
1879
|
// src/index/build.ts
|
|
1735
|
-
import { readFile as
|
|
1736
|
-
import { basename as
|
|
1880
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1881
|
+
import { basename as basename3, extname as extname4 } from "path";
|
|
1737
1882
|
|
|
1738
1883
|
// ../core/src/walk.ts
|
|
1739
1884
|
import { stat } from "fs/promises";
|
|
1740
|
-
import { extname as
|
|
1885
|
+
import { extname as extname3, join as join13, relative, sep } from "path";
|
|
1741
1886
|
import fg from "fast-glob";
|
|
1742
1887
|
var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".heic"]);
|
|
1743
1888
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
|
|
@@ -1752,7 +1897,7 @@ var IGNORE_GLOBS = [
|
|
|
1752
1897
|
function classify(relPath) {
|
|
1753
1898
|
if (relPath.endsWith(".excalidraw.md")) return "note";
|
|
1754
1899
|
if (relPath.endsWith(".excalidraw")) return "excalidraw";
|
|
1755
|
-
const ext =
|
|
1900
|
+
const ext = extname3(relPath).toLowerCase();
|
|
1756
1901
|
if (ext === ".md") return "note";
|
|
1757
1902
|
if (ext === ".canvas") return "canvas";
|
|
1758
1903
|
if (ext === ".pdf") return "pdf";
|
|
@@ -1784,7 +1929,7 @@ async function walkVault(vaultRoot2) {
|
|
|
1784
1929
|
absPath,
|
|
1785
1930
|
relPath,
|
|
1786
1931
|
topDir,
|
|
1787
|
-
ext:
|
|
1932
|
+
ext: extname3(relPath).toLowerCase(),
|
|
1788
1933
|
kind: classify(relPath),
|
|
1789
1934
|
sizeBytes: stats.size,
|
|
1790
1935
|
mtimeMs: stats.mtimeMs
|
|
@@ -1814,12 +1959,12 @@ async function buildIndex(vaultRoot2) {
|
|
|
1814
1959
|
await Promise.all(
|
|
1815
1960
|
noteEntries.map(async (entry) => {
|
|
1816
1961
|
try {
|
|
1817
|
-
const raw = await
|
|
1962
|
+
const raw = await readFile9(entry.absPath, "utf8");
|
|
1818
1963
|
const fm = parseFrontmatter(raw);
|
|
1819
1964
|
const inlineTags = extractInlineTags(fm.body);
|
|
1820
1965
|
const fmTags = frontmatterTags(fm.data);
|
|
1821
1966
|
const allTags = [.../* @__PURE__ */ new Set([...inlineTags, ...fmTags])];
|
|
1822
|
-
const title = typeof fm.data?.title === "string" && fm.data.title ? fm.data.title :
|
|
1967
|
+
const title = typeof fm.data?.title === "string" && fm.data.title ? fm.data.title : basename3(entry.relPath, extname4(entry.relPath));
|
|
1823
1968
|
const doc = {
|
|
1824
1969
|
id: entry.relPath,
|
|
1825
1970
|
title,
|
|
@@ -1867,7 +2012,7 @@ function buildBacklinks(notes2) {
|
|
|
1867
2012
|
|
|
1868
2013
|
// src/init.ts
|
|
1869
2014
|
import { execFileSync } from "child_process";
|
|
1870
|
-
import { access as
|
|
2015
|
+
import { access as access5, copyFile, mkdir as mkdir5, readdir, readFile as readFile10, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
1871
2016
|
import path, { dirname as dirname4, join as join14 } from "path";
|
|
1872
2017
|
function parseInitArgs(argv) {
|
|
1873
2018
|
const opts = { write: false, client: "desktop" };
|
|
@@ -1973,7 +2118,7 @@ async function validateVault(vaultPath) {
|
|
|
1973
2118
|
return { ok: false, error: `Path does not exist: ${vaultPath}` };
|
|
1974
2119
|
}
|
|
1975
2120
|
try {
|
|
1976
|
-
await
|
|
2121
|
+
await access5(join14(vaultPath, ".obsidian"));
|
|
1977
2122
|
} catch {
|
|
1978
2123
|
return {
|
|
1979
2124
|
ok: false,
|
|
@@ -2011,7 +2156,7 @@ async function runInit(opts, deps) {
|
|
|
2011
2156
|
]
|
|
2012
2157
|
};
|
|
2013
2158
|
}
|
|
2014
|
-
const rf = deps.readFile ??
|
|
2159
|
+
const rf = deps.readFile ?? readFile10;
|
|
2015
2160
|
let vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT;
|
|
2016
2161
|
if (!vaultPath) {
|
|
2017
2162
|
const registryPath = obsidianRegistryPath(deps.platform, {
|
|
@@ -2142,12 +2287,12 @@ async function runInit(opts, deps) {
|
|
|
2142
2287
|
}
|
|
2143
2288
|
const merged = opts.client === "vscode" ? mergeSeekstoneConfigVsCode(existing, vaultPath) : mergeSeekstoneConfig(existing, vaultPath);
|
|
2144
2289
|
let backupPath;
|
|
2145
|
-
await
|
|
2290
|
+
await mkdir5(dirname4(configPath), { recursive: true });
|
|
2146
2291
|
if (existingRaw !== null) {
|
|
2147
2292
|
backupPath = `${configPath}.backup-${deps.timestamp}`;
|
|
2148
2293
|
await copyFile(configPath, backupPath);
|
|
2149
2294
|
}
|
|
2150
|
-
await
|
|
2295
|
+
await writeFile2(configPath, `${JSON.stringify(merged, null, 2)}
|
|
2151
2296
|
`, "utf8");
|
|
2152
2297
|
out.push(
|
|
2153
2298
|
`\u2713 Patched ${configPath}`,
|
|
@@ -2218,7 +2363,7 @@ function createLogger(opts = {}) {
|
|
|
2218
2363
|
);
|
|
2219
2364
|
}
|
|
2220
2365
|
}
|
|
2221
|
-
function
|
|
2366
|
+
function writeFile3(record) {
|
|
2222
2367
|
if (!fileOk || filePath === void 0) return;
|
|
2223
2368
|
const line = `${JSON.stringify(record)}
|
|
2224
2369
|
`;
|
|
@@ -2240,7 +2385,7 @@ function createLogger(opts = {}) {
|
|
|
2240
2385
|
const f = fields ?? {};
|
|
2241
2386
|
stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
|
|
2242
2387
|
`);
|
|
2243
|
-
|
|
2388
|
+
writeFile3({ t: ts, level: lvl, msg, ...f });
|
|
2244
2389
|
}
|
|
2245
2390
|
return {
|
|
2246
2391
|
level,
|
|
@@ -2346,7 +2491,7 @@ var ALL_TOOLS = [
|
|
|
2346
2491
|
},
|
|
2347
2492
|
{
|
|
2348
2493
|
name: "read_note",
|
|
2349
|
-
description: "Read a note or a span of it \u2014 by heading section, block reference, or line range. Returns structured JSON with the content, bytes returned,
|
|
2494
|
+
description: "Read a note or a span of it \u2014 by heading section, block reference, or line range. Returns structured JSON with the content, bytes returned, total note size, and a contentHash to pass as prevHash to edit tools for compare-and-swap. Use search or outline_note first to find the right path and section names.",
|
|
2350
2495
|
inputSchema: {
|
|
2351
2496
|
type: "object",
|
|
2352
2497
|
properties: {
|
|
@@ -2420,7 +2565,7 @@ var ALL_TOOLS = [
|
|
|
2420
2565
|
},
|
|
2421
2566
|
{
|
|
2422
2567
|
name: "create_note",
|
|
2423
|
-
description: "Create a new note at a vault-relative path. Optionally sets frontmatter and body content. Parent directories are created automatically. Fails if the note already exists unless overwrite is true.",
|
|
2568
|
+
description: "Create a new note at a vault-relative path. Optionally sets frontmatter and body content. Parent directories are created automatically. Fails if the note already exists unless overwrite is true (prevHash may guard the overwrite).",
|
|
2424
2569
|
inputSchema: {
|
|
2425
2570
|
type: "object",
|
|
2426
2571
|
properties: {
|
|
@@ -2437,6 +2582,10 @@ var ALL_TOOLS = [
|
|
|
2437
2582
|
overwrite: {
|
|
2438
2583
|
type: "boolean",
|
|
2439
2584
|
description: "Overwrite an existing note. Defaults to false."
|
|
2585
|
+
},
|
|
2586
|
+
prevHash: {
|
|
2587
|
+
type: "string",
|
|
2588
|
+
description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
2440
2589
|
}
|
|
2441
2590
|
},
|
|
2442
2591
|
required: ["path"]
|
|
@@ -2444,11 +2593,15 @@ var ALL_TOOLS = [
|
|
|
2444
2593
|
},
|
|
2445
2594
|
{
|
|
2446
2595
|
name: "delete_note",
|
|
2447
|
-
description: "
|
|
2596
|
+
description: "Delete a note. By default it is moved to the vault .trash/ folder (recoverable by moving it back); pass permanent: true to remove it outright.",
|
|
2448
2597
|
inputSchema: {
|
|
2449
2598
|
type: "object",
|
|
2450
2599
|
properties: {
|
|
2451
|
-
path: { type: "string", description: "Vault-relative path of the note to delete." }
|
|
2600
|
+
path: { type: "string", description: "Vault-relative path of the note to delete." },
|
|
2601
|
+
permanent: {
|
|
2602
|
+
type: "boolean",
|
|
2603
|
+
description: "Permanently remove instead of moving to .trash/. Defaults to false."
|
|
2604
|
+
}
|
|
2452
2605
|
},
|
|
2453
2606
|
required: ["path"]
|
|
2454
2607
|
}
|
|
@@ -2480,7 +2633,11 @@ var ALL_TOOLS = [
|
|
|
2480
2633
|
type: "object",
|
|
2481
2634
|
properties: {
|
|
2482
2635
|
path: { type: "string", description: "Vault-relative path to the note." },
|
|
2483
|
-
content: { type: "string", description: "Text to append." }
|
|
2636
|
+
content: { type: "string", description: "Text to append." },
|
|
2637
|
+
prevHash: {
|
|
2638
|
+
type: "string",
|
|
2639
|
+
description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
2640
|
+
}
|
|
2484
2641
|
},
|
|
2485
2642
|
required: ["path", "content"]
|
|
2486
2643
|
}
|
|
@@ -2496,6 +2653,10 @@ var ALL_TOOLS = [
|
|
|
2496
2653
|
type: "object",
|
|
2497
2654
|
description: "Key-value pairs to set. Null value removes the key.",
|
|
2498
2655
|
additionalProperties: true
|
|
2656
|
+
},
|
|
2657
|
+
prevHash: {
|
|
2658
|
+
type: "string",
|
|
2659
|
+
description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
2499
2660
|
}
|
|
2500
2661
|
},
|
|
2501
2662
|
required: ["path", "patch"]
|
|
@@ -2540,6 +2701,10 @@ var ALL_TOOLS = [
|
|
|
2540
2701
|
createIfMissing: {
|
|
2541
2702
|
type: "boolean",
|
|
2542
2703
|
description: "If the heading target is not found, append a new heading (level 2) + content. Only valid for heading targets. Default false."
|
|
2704
|
+
},
|
|
2705
|
+
prevHash: {
|
|
2706
|
+
type: "string",
|
|
2707
|
+
description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
2543
2708
|
}
|
|
2544
2709
|
},
|
|
2545
2710
|
required: ["path", "target", "operation", "content"]
|
|
@@ -2606,6 +2771,10 @@ var ALL_TOOLS = [
|
|
|
2606
2771
|
dryRun: {
|
|
2607
2772
|
type: "boolean",
|
|
2608
2773
|
description: "If true, report matches without writing. Default false."
|
|
2774
|
+
},
|
|
2775
|
+
prevHash: {
|
|
2776
|
+
type: "string",
|
|
2777
|
+
description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
2609
2778
|
}
|
|
2610
2779
|
},
|
|
2611
2780
|
required: ["path", "find", "replace"]
|
|
@@ -2656,6 +2825,10 @@ var ALL_TOOLS = [
|
|
|
2656
2825
|
createIfMissing: {
|
|
2657
2826
|
type: "boolean",
|
|
2658
2827
|
description: "Create the note if it does not exist before appending. Default true."
|
|
2828
|
+
},
|
|
2829
|
+
prevHash: {
|
|
2830
|
+
type: "string",
|
|
2831
|
+
description: "Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
2659
2832
|
}
|
|
2660
2833
|
},
|
|
2661
2834
|
required: ["content"]
|
|
@@ -2669,7 +2842,7 @@ function visibleTools(policy2) {
|
|
|
2669
2842
|
|
|
2670
2843
|
// src/watcher.ts
|
|
2671
2844
|
import { realpathSync } from "fs";
|
|
2672
|
-
import { readFile as
|
|
2845
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2673
2846
|
import { join as join15, relative as relative2, sep as sep2 } from "path";
|
|
2674
2847
|
import chokidar from "chokidar";
|
|
2675
2848
|
function startWatcher(ctx2, log2, opts) {
|
|
@@ -2682,7 +2855,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
2682
2855
|
const toRel = (abs) => relative2(vaultRoot2, abs).split(sep2).join("/");
|
|
2683
2856
|
async function upsert(relPath, op) {
|
|
2684
2857
|
try {
|
|
2685
|
-
const raw = await
|
|
2858
|
+
const raw = await readFile11(join15(vaultRoot2, relPath), "utf8");
|
|
2686
2859
|
removeNoteBacklinks(ctx2, relPath);
|
|
2687
2860
|
upsertDoc(ctx2, buildDoc(relPath, raw));
|
|
2688
2861
|
addNoteBacklinks(ctx2, relPath, raw);
|
|
@@ -2734,7 +2907,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
2734
2907
|
}
|
|
2735
2908
|
|
|
2736
2909
|
// src/index.ts
|
|
2737
|
-
var VERSION = true ? "0.
|
|
2910
|
+
var VERSION = true ? "0.11.0" : "0.0.0-dev";
|
|
2738
2911
|
var intent = parseCliIntent(process.argv.slice(2));
|
|
2739
2912
|
if (intent === "version") {
|
|
2740
2913
|
process.stdout.write(`${VERSION}
|