@velvetmonkey/flywheel-crank 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 +713 -132
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,6 +16,61 @@ import matter from "gray-matter";
|
|
|
16
16
|
var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
|
|
17
17
|
|
|
18
18
|
// src/core/writer.ts
|
|
19
|
+
var SENSITIVE_PATH_PATTERNS = [
|
|
20
|
+
/\.env($|\..*)/i,
|
|
21
|
+
// .env, .env.local, .env.production, etc.
|
|
22
|
+
/\.git\/config$/i,
|
|
23
|
+
// Git config (may contain tokens)
|
|
24
|
+
/\.git\/credentials$/i,
|
|
25
|
+
// Git credentials
|
|
26
|
+
/\.pem$/i,
|
|
27
|
+
// SSL/TLS certificates
|
|
28
|
+
/\.key$/i,
|
|
29
|
+
// Private keys
|
|
30
|
+
/\.p12$/i,
|
|
31
|
+
// PKCS#12 certificates
|
|
32
|
+
/\.pfx$/i,
|
|
33
|
+
// Windows certificate format
|
|
34
|
+
/\.jks$/i,
|
|
35
|
+
// Java keystore
|
|
36
|
+
/id_rsa/i,
|
|
37
|
+
// SSH private key
|
|
38
|
+
/id_ed25519/i,
|
|
39
|
+
// SSH private key (ed25519)
|
|
40
|
+
/id_ecdsa/i,
|
|
41
|
+
// SSH private key (ecdsa)
|
|
42
|
+
/credentials\.json$/i,
|
|
43
|
+
// Cloud credentials files
|
|
44
|
+
/secrets\.json$/i,
|
|
45
|
+
// Secrets files
|
|
46
|
+
/secrets\.ya?ml$/i,
|
|
47
|
+
// Secrets YAML files
|
|
48
|
+
/\.htpasswd$/i,
|
|
49
|
+
// Apache password file
|
|
50
|
+
/shadow$/,
|
|
51
|
+
// Unix shadow password file
|
|
52
|
+
/passwd$/
|
|
53
|
+
// Unix password file
|
|
54
|
+
];
|
|
55
|
+
function isSensitivePath(filePath) {
|
|
56
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
57
|
+
return SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath));
|
|
58
|
+
}
|
|
59
|
+
function detectLineEnding(content) {
|
|
60
|
+
const crlfCount = (content.match(/\r\n/g) || []).length;
|
|
61
|
+
const lfCount = (content.match(/(?<!\r)\n/g) || []).length;
|
|
62
|
+
return crlfCount > lfCount ? "CRLF" : "LF";
|
|
63
|
+
}
|
|
64
|
+
function normalizeLineEndings(content) {
|
|
65
|
+
return content.replace(/\r\n/g, "\n");
|
|
66
|
+
}
|
|
67
|
+
function convertLineEndings(content, style) {
|
|
68
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
69
|
+
return style === "CRLF" ? normalized.replace(/\n/g, "\r\n") : normalized;
|
|
70
|
+
}
|
|
71
|
+
function normalizeTrailingNewline(content) {
|
|
72
|
+
return content.replace(/[\r\n\s]+$/, "") + "\n";
|
|
73
|
+
}
|
|
19
74
|
var EMPTY_PLACEHOLDER_PATTERNS = [
|
|
20
75
|
/^\d+\.\s*$/,
|
|
21
76
|
// "1. " or "2. " (numbered list placeholder)
|
|
@@ -169,25 +224,88 @@ function validatePath(vaultPath2, notePath) {
|
|
|
169
224
|
const resolvedNote = path.resolve(vaultPath2, notePath);
|
|
170
225
|
return resolvedNote.startsWith(resolvedVault);
|
|
171
226
|
}
|
|
227
|
+
async function validatePathSecure(vaultPath2, notePath) {
|
|
228
|
+
const resolvedVault = path.resolve(vaultPath2);
|
|
229
|
+
const resolvedNote = path.resolve(vaultPath2, notePath);
|
|
230
|
+
if (!resolvedNote.startsWith(resolvedVault)) {
|
|
231
|
+
return {
|
|
232
|
+
valid: false,
|
|
233
|
+
reason: "Path traversal not allowed"
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (isSensitivePath(notePath)) {
|
|
237
|
+
return {
|
|
238
|
+
valid: false,
|
|
239
|
+
reason: "Cannot write to sensitive file (credentials, keys, secrets)"
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const fullPath = path.join(vaultPath2, notePath);
|
|
244
|
+
try {
|
|
245
|
+
await fs.access(fullPath);
|
|
246
|
+
const realPath = await fs.realpath(fullPath);
|
|
247
|
+
const realVaultPath = await fs.realpath(vaultPath2);
|
|
248
|
+
if (!realPath.startsWith(realVaultPath)) {
|
|
249
|
+
return {
|
|
250
|
+
valid: false,
|
|
251
|
+
reason: "Symlink target is outside vault"
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const relativePath = path.relative(realVaultPath, realPath);
|
|
255
|
+
if (isSensitivePath(relativePath)) {
|
|
256
|
+
return {
|
|
257
|
+
valid: false,
|
|
258
|
+
reason: "Symlink target is a sensitive file"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
const parentDir = path.dirname(fullPath);
|
|
263
|
+
try {
|
|
264
|
+
await fs.access(parentDir);
|
|
265
|
+
const realParentPath = await fs.realpath(parentDir);
|
|
266
|
+
const realVaultPath = await fs.realpath(vaultPath2);
|
|
267
|
+
if (!realParentPath.startsWith(realVaultPath)) {
|
|
268
|
+
return {
|
|
269
|
+
valid: false,
|
|
270
|
+
reason: "Parent directory symlink target is outside vault"
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
return {
|
|
278
|
+
valid: false,
|
|
279
|
+
reason: `Path validation error: ${error.message}`
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return { valid: true };
|
|
283
|
+
}
|
|
172
284
|
async function readVaultFile(vaultPath2, notePath) {
|
|
173
285
|
if (!validatePath(vaultPath2, notePath)) {
|
|
174
286
|
throw new Error("Invalid path: path traversal not allowed");
|
|
175
287
|
}
|
|
176
288
|
const fullPath = path.join(vaultPath2, notePath);
|
|
177
289
|
const rawContent = await fs.readFile(fullPath, "utf-8");
|
|
178
|
-
const
|
|
290
|
+
const lineEnding = detectLineEnding(rawContent);
|
|
291
|
+
const normalizedContent = normalizeLineEndings(rawContent);
|
|
292
|
+
const parsed = matter(normalizedContent);
|
|
179
293
|
return {
|
|
180
294
|
content: parsed.content,
|
|
181
295
|
frontmatter: parsed.data,
|
|
182
|
-
rawContent
|
|
296
|
+
rawContent,
|
|
297
|
+
lineEnding
|
|
183
298
|
};
|
|
184
299
|
}
|
|
185
|
-
async function writeVaultFile(vaultPath2, notePath, content, frontmatter) {
|
|
186
|
-
|
|
187
|
-
|
|
300
|
+
async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEnding = "LF") {
|
|
301
|
+
const validation = await validatePathSecure(vaultPath2, notePath);
|
|
302
|
+
if (!validation.valid) {
|
|
303
|
+
throw new Error(`Invalid path: ${validation.reason}`);
|
|
188
304
|
}
|
|
189
305
|
const fullPath = path.join(vaultPath2, notePath);
|
|
190
|
-
|
|
306
|
+
let output = matter.stringify(content, frontmatter);
|
|
307
|
+
output = normalizeTrailingNewline(output);
|
|
308
|
+
output = convertLineEndings(output, lineEnding);
|
|
191
309
|
await fs.writeFile(fullPath, output, "utf-8");
|
|
192
310
|
}
|
|
193
311
|
function removeFromSection(content, section, pattern, mode = "first", useRegex = false) {
|
|
@@ -374,10 +492,498 @@ import {
|
|
|
374
492
|
loadEntityCache,
|
|
375
493
|
saveEntityCache
|
|
376
494
|
} from "@velvetmonkey/vault-core";
|
|
495
|
+
import path4 from "path";
|
|
496
|
+
|
|
497
|
+
// src/core/stemmer.ts
|
|
498
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
499
|
+
"the",
|
|
500
|
+
"a",
|
|
501
|
+
"an",
|
|
502
|
+
"and",
|
|
503
|
+
"or",
|
|
504
|
+
"but",
|
|
505
|
+
"in",
|
|
506
|
+
"on",
|
|
507
|
+
"at",
|
|
508
|
+
"to",
|
|
509
|
+
"for",
|
|
510
|
+
"of",
|
|
511
|
+
"with",
|
|
512
|
+
"by",
|
|
513
|
+
"from",
|
|
514
|
+
"as",
|
|
515
|
+
"is",
|
|
516
|
+
"was",
|
|
517
|
+
"are",
|
|
518
|
+
"were",
|
|
519
|
+
"been",
|
|
520
|
+
"be",
|
|
521
|
+
"have",
|
|
522
|
+
"has",
|
|
523
|
+
"had",
|
|
524
|
+
"do",
|
|
525
|
+
"does",
|
|
526
|
+
"did",
|
|
527
|
+
"will",
|
|
528
|
+
"would",
|
|
529
|
+
"could",
|
|
530
|
+
"should",
|
|
531
|
+
"may",
|
|
532
|
+
"might",
|
|
533
|
+
"must",
|
|
534
|
+
"shall",
|
|
535
|
+
"can",
|
|
536
|
+
"need",
|
|
537
|
+
"this",
|
|
538
|
+
"that",
|
|
539
|
+
"these",
|
|
540
|
+
"those",
|
|
541
|
+
"i",
|
|
542
|
+
"you",
|
|
543
|
+
"he",
|
|
544
|
+
"she",
|
|
545
|
+
"it",
|
|
546
|
+
"we",
|
|
547
|
+
"they",
|
|
548
|
+
"what",
|
|
549
|
+
"which",
|
|
550
|
+
"who",
|
|
551
|
+
"whom",
|
|
552
|
+
"when",
|
|
553
|
+
"where",
|
|
554
|
+
"why",
|
|
555
|
+
"how",
|
|
556
|
+
"all",
|
|
557
|
+
"each",
|
|
558
|
+
"every",
|
|
559
|
+
"both",
|
|
560
|
+
"few",
|
|
561
|
+
"more",
|
|
562
|
+
"most",
|
|
563
|
+
"other",
|
|
564
|
+
"some",
|
|
565
|
+
"such",
|
|
566
|
+
"no",
|
|
567
|
+
"not",
|
|
568
|
+
"only",
|
|
569
|
+
"own",
|
|
570
|
+
"same",
|
|
571
|
+
"so",
|
|
572
|
+
"than",
|
|
573
|
+
"too",
|
|
574
|
+
"very",
|
|
575
|
+
"just",
|
|
576
|
+
"also",
|
|
577
|
+
"about",
|
|
578
|
+
"after",
|
|
579
|
+
"before",
|
|
580
|
+
"being",
|
|
581
|
+
"between",
|
|
582
|
+
"into",
|
|
583
|
+
"through",
|
|
584
|
+
"during",
|
|
585
|
+
"above",
|
|
586
|
+
"below",
|
|
587
|
+
"out",
|
|
588
|
+
"off",
|
|
589
|
+
"over",
|
|
590
|
+
"under",
|
|
591
|
+
"again",
|
|
592
|
+
"further",
|
|
593
|
+
"then",
|
|
594
|
+
"once",
|
|
595
|
+
"here",
|
|
596
|
+
"there",
|
|
597
|
+
"any",
|
|
598
|
+
"now",
|
|
599
|
+
"new",
|
|
600
|
+
"even",
|
|
601
|
+
"much",
|
|
602
|
+
"back",
|
|
603
|
+
"going"
|
|
604
|
+
]);
|
|
605
|
+
function isConsonant(word, i) {
|
|
606
|
+
const c = word[i];
|
|
607
|
+
if (c === "a" || c === "e" || c === "i" || c === "o" || c === "u") {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
if (c === "y") {
|
|
611
|
+
return i === 0 || !isConsonant(word, i - 1);
|
|
612
|
+
}
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
function measure(word, end) {
|
|
616
|
+
let n = 0;
|
|
617
|
+
let i = 0;
|
|
618
|
+
while (i <= end) {
|
|
619
|
+
if (!isConsonant(word, i))
|
|
620
|
+
break;
|
|
621
|
+
i++;
|
|
622
|
+
}
|
|
623
|
+
if (i > end)
|
|
624
|
+
return n;
|
|
625
|
+
i++;
|
|
626
|
+
while (true) {
|
|
627
|
+
while (i <= end) {
|
|
628
|
+
if (isConsonant(word, i))
|
|
629
|
+
break;
|
|
630
|
+
i++;
|
|
631
|
+
}
|
|
632
|
+
if (i > end)
|
|
633
|
+
return n;
|
|
634
|
+
n++;
|
|
635
|
+
i++;
|
|
636
|
+
while (i <= end) {
|
|
637
|
+
if (!isConsonant(word, i))
|
|
638
|
+
break;
|
|
639
|
+
i++;
|
|
640
|
+
}
|
|
641
|
+
if (i > end)
|
|
642
|
+
return n;
|
|
643
|
+
i++;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function hasVowel(word, end) {
|
|
647
|
+
for (let i = 0; i <= end; i++) {
|
|
648
|
+
if (!isConsonant(word, i))
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
function endsWithDoubleConsonant(word, end) {
|
|
654
|
+
if (end < 1)
|
|
655
|
+
return false;
|
|
656
|
+
if (word[end] !== word[end - 1])
|
|
657
|
+
return false;
|
|
658
|
+
return isConsonant(word, end);
|
|
659
|
+
}
|
|
660
|
+
function cvcPattern(word, i) {
|
|
661
|
+
if (i < 2)
|
|
662
|
+
return false;
|
|
663
|
+
if (!isConsonant(word, i) || isConsonant(word, i - 1) || !isConsonant(word, i - 2)) {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
const c = word[i];
|
|
667
|
+
return c !== "w" && c !== "x" && c !== "y";
|
|
668
|
+
}
|
|
669
|
+
function replaceSuffix(word, suffix, replacement, minMeasure) {
|
|
670
|
+
if (!word.endsWith(suffix))
|
|
671
|
+
return word;
|
|
672
|
+
const stem2 = word.slice(0, word.length - suffix.length);
|
|
673
|
+
if (measure(stem2, stem2.length - 1) > minMeasure) {
|
|
674
|
+
return stem2 + replacement;
|
|
675
|
+
}
|
|
676
|
+
return word;
|
|
677
|
+
}
|
|
678
|
+
function step1a(word) {
|
|
679
|
+
if (word.endsWith("sses")) {
|
|
680
|
+
return word.slice(0, -2);
|
|
681
|
+
}
|
|
682
|
+
if (word.endsWith("ies")) {
|
|
683
|
+
return word.slice(0, -2);
|
|
684
|
+
}
|
|
685
|
+
if (word.endsWith("ss")) {
|
|
686
|
+
return word;
|
|
687
|
+
}
|
|
688
|
+
if (word.endsWith("s")) {
|
|
689
|
+
return word.slice(0, -1);
|
|
690
|
+
}
|
|
691
|
+
return word;
|
|
692
|
+
}
|
|
693
|
+
function step1b(word) {
|
|
694
|
+
if (word.endsWith("eed")) {
|
|
695
|
+
const stem3 = word.slice(0, -3);
|
|
696
|
+
if (measure(stem3, stem3.length - 1) > 0) {
|
|
697
|
+
return stem3 + "ee";
|
|
698
|
+
}
|
|
699
|
+
return word;
|
|
700
|
+
}
|
|
701
|
+
let stem2 = "";
|
|
702
|
+
let didRemove = false;
|
|
703
|
+
if (word.endsWith("ed")) {
|
|
704
|
+
stem2 = word.slice(0, -2);
|
|
705
|
+
if (hasVowel(stem2, stem2.length - 1)) {
|
|
706
|
+
word = stem2;
|
|
707
|
+
didRemove = true;
|
|
708
|
+
}
|
|
709
|
+
} else if (word.endsWith("ing")) {
|
|
710
|
+
stem2 = word.slice(0, -3);
|
|
711
|
+
if (hasVowel(stem2, stem2.length - 1)) {
|
|
712
|
+
word = stem2;
|
|
713
|
+
didRemove = true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (didRemove) {
|
|
717
|
+
if (word.endsWith("at") || word.endsWith("bl") || word.endsWith("iz")) {
|
|
718
|
+
return word + "e";
|
|
719
|
+
}
|
|
720
|
+
if (endsWithDoubleConsonant(word, word.length - 1)) {
|
|
721
|
+
const c = word[word.length - 1];
|
|
722
|
+
if (c !== "l" && c !== "s" && c !== "z") {
|
|
723
|
+
return word.slice(0, -1);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (measure(word, word.length - 1) === 1 && cvcPattern(word, word.length - 1)) {
|
|
727
|
+
return word + "e";
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return word;
|
|
731
|
+
}
|
|
732
|
+
function step1c(word) {
|
|
733
|
+
if (word.endsWith("y")) {
|
|
734
|
+
const stem2 = word.slice(0, -1);
|
|
735
|
+
if (hasVowel(stem2, stem2.length - 1)) {
|
|
736
|
+
return stem2 + "i";
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return word;
|
|
740
|
+
}
|
|
741
|
+
function step2(word) {
|
|
742
|
+
const suffixes = [
|
|
743
|
+
["ational", "ate"],
|
|
744
|
+
["tional", "tion"],
|
|
745
|
+
["enci", "ence"],
|
|
746
|
+
["anci", "ance"],
|
|
747
|
+
["izer", "ize"],
|
|
748
|
+
["abli", "able"],
|
|
749
|
+
["alli", "al"],
|
|
750
|
+
["entli", "ent"],
|
|
751
|
+
["eli", "e"],
|
|
752
|
+
["ousli", "ous"],
|
|
753
|
+
["ization", "ize"],
|
|
754
|
+
["ation", "ate"],
|
|
755
|
+
["ator", "ate"],
|
|
756
|
+
["alism", "al"],
|
|
757
|
+
["iveness", "ive"],
|
|
758
|
+
["fulness", "ful"],
|
|
759
|
+
["ousness", "ous"],
|
|
760
|
+
["aliti", "al"],
|
|
761
|
+
["iviti", "ive"],
|
|
762
|
+
["biliti", "ble"]
|
|
763
|
+
];
|
|
764
|
+
for (const [suffix, replacement] of suffixes) {
|
|
765
|
+
if (word.endsWith(suffix)) {
|
|
766
|
+
return replaceSuffix(word, suffix, replacement, 0);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return word;
|
|
770
|
+
}
|
|
771
|
+
function step3(word) {
|
|
772
|
+
const suffixes = [
|
|
773
|
+
["icate", "ic"],
|
|
774
|
+
["ative", ""],
|
|
775
|
+
["alize", "al"],
|
|
776
|
+
["iciti", "ic"],
|
|
777
|
+
["ical", "ic"],
|
|
778
|
+
["ful", ""],
|
|
779
|
+
["ness", ""]
|
|
780
|
+
];
|
|
781
|
+
for (const [suffix, replacement] of suffixes) {
|
|
782
|
+
if (word.endsWith(suffix)) {
|
|
783
|
+
return replaceSuffix(word, suffix, replacement, 0);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return word;
|
|
787
|
+
}
|
|
788
|
+
function step4(word) {
|
|
789
|
+
const suffixes = [
|
|
790
|
+
"al",
|
|
791
|
+
"ance",
|
|
792
|
+
"ence",
|
|
793
|
+
"er",
|
|
794
|
+
"ic",
|
|
795
|
+
"able",
|
|
796
|
+
"ible",
|
|
797
|
+
"ant",
|
|
798
|
+
"ement",
|
|
799
|
+
"ment",
|
|
800
|
+
"ent",
|
|
801
|
+
"ion",
|
|
802
|
+
"ou",
|
|
803
|
+
"ism",
|
|
804
|
+
"ate",
|
|
805
|
+
"iti",
|
|
806
|
+
"ous",
|
|
807
|
+
"ive",
|
|
808
|
+
"ize"
|
|
809
|
+
];
|
|
810
|
+
for (const suffix of suffixes) {
|
|
811
|
+
if (word.endsWith(suffix)) {
|
|
812
|
+
const stem2 = word.slice(0, word.length - suffix.length);
|
|
813
|
+
if (measure(stem2, stem2.length - 1) > 1) {
|
|
814
|
+
if (suffix === "ion") {
|
|
815
|
+
const lastChar = stem2[stem2.length - 1];
|
|
816
|
+
if (lastChar === "s" || lastChar === "t") {
|
|
817
|
+
return stem2;
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
return stem2;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return word;
|
|
826
|
+
}
|
|
827
|
+
function step5a(word) {
|
|
828
|
+
if (word.endsWith("e")) {
|
|
829
|
+
const stem2 = word.slice(0, -1);
|
|
830
|
+
const m = measure(stem2, stem2.length - 1);
|
|
831
|
+
if (m > 1) {
|
|
832
|
+
return stem2;
|
|
833
|
+
}
|
|
834
|
+
if (m === 1 && !cvcPattern(stem2, stem2.length - 1)) {
|
|
835
|
+
return stem2;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return word;
|
|
839
|
+
}
|
|
840
|
+
function step5b(word) {
|
|
841
|
+
if (word.endsWith("ll")) {
|
|
842
|
+
const stem2 = word.slice(0, -1);
|
|
843
|
+
if (measure(stem2, stem2.length - 1) > 1) {
|
|
844
|
+
return stem2;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return word;
|
|
848
|
+
}
|
|
849
|
+
function stem(word) {
|
|
850
|
+
word = word.toLowerCase();
|
|
851
|
+
if (word.length < 3) {
|
|
852
|
+
return word;
|
|
853
|
+
}
|
|
854
|
+
word = step1a(word);
|
|
855
|
+
word = step1b(word);
|
|
856
|
+
word = step1c(word);
|
|
857
|
+
word = step2(word);
|
|
858
|
+
word = step3(word);
|
|
859
|
+
word = step4(word);
|
|
860
|
+
word = step5a(word);
|
|
861
|
+
word = step5b(word);
|
|
862
|
+
return word;
|
|
863
|
+
}
|
|
864
|
+
function tokenize(text) {
|
|
865
|
+
const cleanText = text.replace(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g, "$1").replace(/[*_`#\[\]()]/g, " ").toLowerCase();
|
|
866
|
+
const words = cleanText.match(/\b[a-z]{4,}\b/g) || [];
|
|
867
|
+
return words.filter((word) => !STOPWORDS.has(word));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/core/cooccurrence.ts
|
|
871
|
+
import { readdir, readFile } from "fs/promises";
|
|
377
872
|
import path3 from "path";
|
|
873
|
+
var DEFAULT_MIN_COOCCURRENCE = 2;
|
|
874
|
+
var EXCLUDED_FOLDERS = /* @__PURE__ */ new Set([
|
|
875
|
+
"templates",
|
|
876
|
+
".obsidian",
|
|
877
|
+
".claude",
|
|
878
|
+
".git"
|
|
879
|
+
]);
|
|
880
|
+
function noteContainsEntity(content, entityName) {
|
|
881
|
+
const entityTokens = tokenize(entityName);
|
|
882
|
+
if (entityTokens.length === 0)
|
|
883
|
+
return false;
|
|
884
|
+
const contentTokens = new Set(tokenize(content));
|
|
885
|
+
let matchCount = 0;
|
|
886
|
+
for (const token of entityTokens) {
|
|
887
|
+
if (contentTokens.has(token)) {
|
|
888
|
+
matchCount++;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (entityTokens.length === 1) {
|
|
892
|
+
return matchCount === 1;
|
|
893
|
+
}
|
|
894
|
+
return matchCount / entityTokens.length >= 0.5;
|
|
895
|
+
}
|
|
896
|
+
function incrementCooccurrence(associations, entityA, entityB) {
|
|
897
|
+
if (!associations[entityA]) {
|
|
898
|
+
associations[entityA] = /* @__PURE__ */ new Map();
|
|
899
|
+
}
|
|
900
|
+
const current = associations[entityA].get(entityB) || 0;
|
|
901
|
+
associations[entityA].set(entityB, current + 1);
|
|
902
|
+
}
|
|
903
|
+
async function* walkMarkdownFiles(dir, baseDir) {
|
|
904
|
+
try {
|
|
905
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
906
|
+
for (const entry of entries) {
|
|
907
|
+
const fullPath = path3.join(dir, entry.name);
|
|
908
|
+
const relativePath = path3.relative(baseDir, fullPath);
|
|
909
|
+
const topFolder = relativePath.split(path3.sep)[0];
|
|
910
|
+
if (EXCLUDED_FOLDERS.has(topFolder)) {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (entry.isDirectory()) {
|
|
914
|
+
yield* walkMarkdownFiles(fullPath, baseDir);
|
|
915
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
916
|
+
yield { path: fullPath, relativePath };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
} catch {
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function mineCooccurrences(vaultPath2, entities, options = {}) {
|
|
923
|
+
const { minCount = DEFAULT_MIN_COOCCURRENCE } = options;
|
|
924
|
+
const associations = {};
|
|
925
|
+
let notesScanned = 0;
|
|
926
|
+
const validEntities = entities.filter((e) => e.length <= 30);
|
|
927
|
+
for await (const file of walkMarkdownFiles(vaultPath2, vaultPath2)) {
|
|
928
|
+
try {
|
|
929
|
+
const content = await readFile(file.path, "utf-8");
|
|
930
|
+
notesScanned++;
|
|
931
|
+
const mentionedEntities = [];
|
|
932
|
+
for (const entity of validEntities) {
|
|
933
|
+
if (noteContainsEntity(content, entity)) {
|
|
934
|
+
mentionedEntities.push(entity);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
for (const entityA of mentionedEntities) {
|
|
938
|
+
for (const entityB of mentionedEntities) {
|
|
939
|
+
if (entityA !== entityB) {
|
|
940
|
+
incrementCooccurrence(associations, entityA, entityB);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
let totalAssociations = 0;
|
|
948
|
+
for (const entityAssocs of Object.values(associations)) {
|
|
949
|
+
for (const count of entityAssocs.values()) {
|
|
950
|
+
if (count >= minCount) {
|
|
951
|
+
totalAssociations++;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
associations,
|
|
957
|
+
minCount,
|
|
958
|
+
_metadata: {
|
|
959
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
960
|
+
total_associations: totalAssociations,
|
|
961
|
+
notes_scanned: notesScanned
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function getCooccurrenceBoost(entityName, matchedEntities, cooccurrenceIndex2) {
|
|
966
|
+
if (!cooccurrenceIndex2)
|
|
967
|
+
return 0;
|
|
968
|
+
let boost = 0;
|
|
969
|
+
const { associations, minCount } = cooccurrenceIndex2;
|
|
970
|
+
for (const matched of matchedEntities) {
|
|
971
|
+
const entityAssocs = associations[matched];
|
|
972
|
+
if (entityAssocs) {
|
|
973
|
+
const count = entityAssocs.get(entityName) || 0;
|
|
974
|
+
if (count >= minCount) {
|
|
975
|
+
boost += 3;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return boost;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/core/wikilinks.ts
|
|
378
983
|
var entityIndex = null;
|
|
379
984
|
var indexReady = false;
|
|
380
985
|
var indexError = null;
|
|
986
|
+
var cooccurrenceIndex = null;
|
|
381
987
|
var DEFAULT_EXCLUDE_FOLDERS = [
|
|
382
988
|
"daily-notes",
|
|
383
989
|
"daily",
|
|
@@ -390,7 +996,7 @@ var DEFAULT_EXCLUDE_FOLDERS = [
|
|
|
390
996
|
"templates"
|
|
391
997
|
];
|
|
392
998
|
async function initializeEntityIndex(vaultPath2) {
|
|
393
|
-
const cacheFile =
|
|
999
|
+
const cacheFile = path4.join(vaultPath2, ".claude", "wikilink-entities.json");
|
|
394
1000
|
try {
|
|
395
1001
|
const cached = await loadEntityCache(cacheFile);
|
|
396
1002
|
if (cached) {
|
|
@@ -416,8 +1022,17 @@ async function rebuildIndex(vaultPath2, cacheFile) {
|
|
|
416
1022
|
excludeFolders: DEFAULT_EXCLUDE_FOLDERS
|
|
417
1023
|
});
|
|
418
1024
|
indexReady = true;
|
|
419
|
-
const
|
|
420
|
-
console.error(`[Crank] Entity index built: ${entityIndex._metadata.total_entities} entities in ${
|
|
1025
|
+
const entityDuration = Date.now() - startTime;
|
|
1026
|
+
console.error(`[Crank] Entity index built: ${entityIndex._metadata.total_entities} entities in ${entityDuration}ms`);
|
|
1027
|
+
try {
|
|
1028
|
+
const cooccurrenceStart = Date.now();
|
|
1029
|
+
const entities = getAllEntities(entityIndex);
|
|
1030
|
+
cooccurrenceIndex = await mineCooccurrences(vaultPath2, entities);
|
|
1031
|
+
const cooccurrenceDuration = Date.now() - cooccurrenceStart;
|
|
1032
|
+
console.error(`[Crank] Co-occurrence index built: ${cooccurrenceIndex._metadata.total_associations} associations in ${cooccurrenceDuration}ms`);
|
|
1033
|
+
} catch (e) {
|
|
1034
|
+
console.error(`[Crank] Failed to build co-occurrence index: ${e}`);
|
|
1035
|
+
}
|
|
421
1036
|
try {
|
|
422
1037
|
await saveEntityCache(cacheFile, entityIndex);
|
|
423
1038
|
console.error(`[Crank] Entity cache saved`);
|
|
@@ -461,86 +1076,6 @@ function maybeApplyWikilinks(content, skipWikilinks) {
|
|
|
461
1076
|
return { content: result.content };
|
|
462
1077
|
}
|
|
463
1078
|
var SUGGESTION_PATTERN = /→\s*\[\[.+$/;
|
|
464
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
465
|
-
"the",
|
|
466
|
-
"a",
|
|
467
|
-
"an",
|
|
468
|
-
"and",
|
|
469
|
-
"or",
|
|
470
|
-
"but",
|
|
471
|
-
"in",
|
|
472
|
-
"on",
|
|
473
|
-
"at",
|
|
474
|
-
"to",
|
|
475
|
-
"for",
|
|
476
|
-
"of",
|
|
477
|
-
"with",
|
|
478
|
-
"by",
|
|
479
|
-
"from",
|
|
480
|
-
"as",
|
|
481
|
-
"is",
|
|
482
|
-
"was",
|
|
483
|
-
"are",
|
|
484
|
-
"were",
|
|
485
|
-
"been",
|
|
486
|
-
"be",
|
|
487
|
-
"have",
|
|
488
|
-
"has",
|
|
489
|
-
"had",
|
|
490
|
-
"do",
|
|
491
|
-
"does",
|
|
492
|
-
"did",
|
|
493
|
-
"will",
|
|
494
|
-
"would",
|
|
495
|
-
"could",
|
|
496
|
-
"should",
|
|
497
|
-
"may",
|
|
498
|
-
"might",
|
|
499
|
-
"must",
|
|
500
|
-
"shall",
|
|
501
|
-
"can",
|
|
502
|
-
"need",
|
|
503
|
-
"this",
|
|
504
|
-
"that",
|
|
505
|
-
"these",
|
|
506
|
-
"those",
|
|
507
|
-
"i",
|
|
508
|
-
"you",
|
|
509
|
-
"he",
|
|
510
|
-
"she",
|
|
511
|
-
"it",
|
|
512
|
-
"we",
|
|
513
|
-
"they",
|
|
514
|
-
"what",
|
|
515
|
-
"which",
|
|
516
|
-
"who",
|
|
517
|
-
"whom",
|
|
518
|
-
"when",
|
|
519
|
-
"where",
|
|
520
|
-
"why",
|
|
521
|
-
"how",
|
|
522
|
-
"all",
|
|
523
|
-
"each",
|
|
524
|
-
"every",
|
|
525
|
-
"both",
|
|
526
|
-
"few",
|
|
527
|
-
"more",
|
|
528
|
-
"most",
|
|
529
|
-
"other",
|
|
530
|
-
"some",
|
|
531
|
-
"such",
|
|
532
|
-
"no",
|
|
533
|
-
"not",
|
|
534
|
-
"only",
|
|
535
|
-
"own",
|
|
536
|
-
"same",
|
|
537
|
-
"so",
|
|
538
|
-
"than",
|
|
539
|
-
"too",
|
|
540
|
-
"very",
|
|
541
|
-
"just",
|
|
542
|
-
"also"
|
|
543
|
-
]);
|
|
544
1079
|
function extractLinkedEntities(content) {
|
|
545
1080
|
const linked = /* @__PURE__ */ new Set();
|
|
546
1081
|
const wikilinkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
@@ -550,23 +1085,37 @@ function extractLinkedEntities(content) {
|
|
|
550
1085
|
}
|
|
551
1086
|
return linked;
|
|
552
1087
|
}
|
|
553
|
-
function
|
|
554
|
-
const
|
|
555
|
-
const
|
|
556
|
-
|
|
1088
|
+
function tokenizeForMatching(content) {
|
|
1089
|
+
const tokens = tokenize(content);
|
|
1090
|
+
const tokenSet = new Set(tokens);
|
|
1091
|
+
const stems = new Set(tokens.map((t) => stem(t)));
|
|
1092
|
+
return { tokens: tokenSet, stems };
|
|
557
1093
|
}
|
|
558
|
-
|
|
559
|
-
|
|
1094
|
+
var MAX_ENTITY_LENGTH = 30;
|
|
1095
|
+
var MIN_SUGGESTION_SCORE = 5;
|
|
1096
|
+
var MIN_MATCH_RATIO = 0.4;
|
|
1097
|
+
function scoreEntity(entityName, contentTokens, contentStems) {
|
|
1098
|
+
const entityTokens = tokenize(entityName);
|
|
1099
|
+
if (entityTokens.length === 0)
|
|
1100
|
+
return 0;
|
|
1101
|
+
const entityStems = entityTokens.map((t) => stem(t));
|
|
560
1102
|
let score = 0;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
1103
|
+
let matchedWords = 0;
|
|
1104
|
+
for (let i = 0; i < entityTokens.length; i++) {
|
|
1105
|
+
const token = entityTokens[i];
|
|
1106
|
+
const entityStem = entityStems[i];
|
|
1107
|
+
if (contentTokens.has(token)) {
|
|
1108
|
+
score += 10;
|
|
1109
|
+
matchedWords++;
|
|
1110
|
+
} else if (contentStems.has(entityStem)) {
|
|
1111
|
+
score += 5;
|
|
1112
|
+
matchedWords++;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (entityTokens.length > 1) {
|
|
1116
|
+
const matchRatio = matchedWords / entityTokens.length;
|
|
1117
|
+
if (matchRatio < MIN_MATCH_RATIO) {
|
|
1118
|
+
return 0;
|
|
570
1119
|
}
|
|
571
1120
|
}
|
|
572
1121
|
return score;
|
|
@@ -584,19 +1133,49 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
584
1133
|
if (entities.length === 0) {
|
|
585
1134
|
return emptyResult;
|
|
586
1135
|
}
|
|
587
|
-
const contentTokens =
|
|
588
|
-
if (contentTokens.
|
|
1136
|
+
const { tokens: contentTokens, stems: contentStems } = tokenizeForMatching(content);
|
|
1137
|
+
if (contentTokens.size === 0) {
|
|
589
1138
|
return emptyResult;
|
|
590
1139
|
}
|
|
591
1140
|
const linkedEntities = excludeLinked ? extractLinkedEntities(content) : /* @__PURE__ */ new Set();
|
|
592
1141
|
const scoredEntities = [];
|
|
1142
|
+
const directlyMatchedEntities = /* @__PURE__ */ new Set();
|
|
593
1143
|
for (const entity of entities) {
|
|
594
|
-
|
|
1144
|
+
const entityName = typeof entity === "string" ? entity : entity.name;
|
|
1145
|
+
if (!entityName)
|
|
1146
|
+
continue;
|
|
1147
|
+
if (entityName.length > MAX_ENTITY_LENGTH) {
|
|
595
1148
|
continue;
|
|
596
1149
|
}
|
|
597
|
-
|
|
1150
|
+
if (linkedEntities.has(entityName.toLowerCase())) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
const score = scoreEntity(entityName, contentTokens, contentStems);
|
|
598
1154
|
if (score > 0) {
|
|
599
|
-
|
|
1155
|
+
directlyMatchedEntities.add(entityName);
|
|
1156
|
+
}
|
|
1157
|
+
if (score >= MIN_SUGGESTION_SCORE) {
|
|
1158
|
+
scoredEntities.push({ name: entityName, score });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (cooccurrenceIndex && directlyMatchedEntities.size > 0) {
|
|
1162
|
+
for (const entity of entities) {
|
|
1163
|
+
const entityName = typeof entity === "string" ? entity : entity.name;
|
|
1164
|
+
if (!entityName)
|
|
1165
|
+
continue;
|
|
1166
|
+
if (entityName.length > MAX_ENTITY_LENGTH)
|
|
1167
|
+
continue;
|
|
1168
|
+
if (linkedEntities.has(entityName.toLowerCase()))
|
|
1169
|
+
continue;
|
|
1170
|
+
const boost = getCooccurrenceBoost(entityName, directlyMatchedEntities, cooccurrenceIndex);
|
|
1171
|
+
if (boost > 0) {
|
|
1172
|
+
const existing = scoredEntities.find((e) => e.name === entityName);
|
|
1173
|
+
if (existing) {
|
|
1174
|
+
existing.score += boost;
|
|
1175
|
+
} else if (boost >= MIN_SUGGESTION_SCORE) {
|
|
1176
|
+
scoredEntities.push({ name: entityName, score: boost });
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
600
1179
|
}
|
|
601
1180
|
}
|
|
602
1181
|
scoredEntities.sort((a, b) => b.score - a.score);
|
|
@@ -613,7 +1192,7 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
613
1192
|
|
|
614
1193
|
// src/tools/mutations.ts
|
|
615
1194
|
import fs2 from "fs/promises";
|
|
616
|
-
import
|
|
1195
|
+
import path5 from "path";
|
|
617
1196
|
function registerMutationTools(server2, vaultPath2) {
|
|
618
1197
|
server2.tool(
|
|
619
1198
|
"vault_add_to_section",
|
|
@@ -631,7 +1210,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
631
1210
|
},
|
|
632
1211
|
async ({ path: notePath, section, content, position, format, commit, skipWikilinks, preserveListNesting, suggestOutgoingLinks }) => {
|
|
633
1212
|
try {
|
|
634
|
-
const fullPath =
|
|
1213
|
+
const fullPath = path5.join(vaultPath2, notePath);
|
|
635
1214
|
try {
|
|
636
1215
|
await fs2.access(fullPath);
|
|
637
1216
|
} catch {
|
|
@@ -715,7 +1294,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
715
1294
|
},
|
|
716
1295
|
async ({ path: notePath, section, pattern, mode, useRegex, commit }) => {
|
|
717
1296
|
try {
|
|
718
|
-
const fullPath =
|
|
1297
|
+
const fullPath = path5.join(vaultPath2, notePath);
|
|
719
1298
|
try {
|
|
720
1299
|
await fs2.access(fullPath);
|
|
721
1300
|
} catch {
|
|
@@ -797,7 +1376,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
797
1376
|
},
|
|
798
1377
|
async ({ path: notePath, section, search, replacement, mode, useRegex, commit, skipWikilinks, suggestOutgoingLinks }) => {
|
|
799
1378
|
try {
|
|
800
|
-
const fullPath =
|
|
1379
|
+
const fullPath = path5.join(vaultPath2, notePath);
|
|
801
1380
|
try {
|
|
802
1381
|
await fs2.access(fullPath);
|
|
803
1382
|
} catch {
|
|
@@ -883,7 +1462,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
883
1462
|
// src/tools/tasks.ts
|
|
884
1463
|
import { z as z2 } from "zod";
|
|
885
1464
|
import fs3 from "fs/promises";
|
|
886
|
-
import
|
|
1465
|
+
import path6 from "path";
|
|
887
1466
|
var TASK_REGEX = /^(\s*)-\s*\[([ xX])\]\s*(.*)$/;
|
|
888
1467
|
function findTasks(content, section) {
|
|
889
1468
|
const lines = content.split("\n");
|
|
@@ -939,7 +1518,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
939
1518
|
},
|
|
940
1519
|
async ({ path: notePath, task, section, commit }) => {
|
|
941
1520
|
try {
|
|
942
|
-
const fullPath =
|
|
1521
|
+
const fullPath = path6.join(vaultPath2, notePath);
|
|
943
1522
|
try {
|
|
944
1523
|
await fs3.access(fullPath);
|
|
945
1524
|
} catch {
|
|
@@ -1029,11 +1608,12 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
1029
1608
|
completed: z2.boolean().default(false).describe("Whether the task should start as completed"),
|
|
1030
1609
|
commit: z2.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
|
|
1031
1610
|
skipWikilinks: z2.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
|
|
1032
|
-
suggestOutgoingLinks: z2.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.')
|
|
1611
|
+
suggestOutgoingLinks: z2.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.'),
|
|
1612
|
+
preserveListNesting: z2.boolean().default(true).describe("Preserve indentation when inserting into nested lists. Default: true")
|
|
1033
1613
|
},
|
|
1034
|
-
async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks }) => {
|
|
1614
|
+
async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks, preserveListNesting }) => {
|
|
1035
1615
|
try {
|
|
1036
|
-
const fullPath =
|
|
1616
|
+
const fullPath = path6.join(vaultPath2, notePath);
|
|
1037
1617
|
try {
|
|
1038
1618
|
await fs3.access(fullPath);
|
|
1039
1619
|
} catch {
|
|
@@ -1069,7 +1649,8 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
1069
1649
|
fileContent,
|
|
1070
1650
|
sectionBoundary,
|
|
1071
1651
|
taskLine,
|
|
1072
|
-
position
|
|
1652
|
+
position,
|
|
1653
|
+
{ preserveListNesting }
|
|
1073
1654
|
);
|
|
1074
1655
|
await writeVaultFile(vaultPath2, notePath, updatedContent, frontmatter);
|
|
1075
1656
|
let gitCommit;
|
|
@@ -1109,7 +1690,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
1109
1690
|
// src/tools/frontmatter.ts
|
|
1110
1691
|
import { z as z3 } from "zod";
|
|
1111
1692
|
import fs4 from "fs/promises";
|
|
1112
|
-
import
|
|
1693
|
+
import path7 from "path";
|
|
1113
1694
|
function registerFrontmatterTools(server2, vaultPath2) {
|
|
1114
1695
|
server2.tool(
|
|
1115
1696
|
"vault_update_frontmatter",
|
|
@@ -1121,7 +1702,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
1121
1702
|
},
|
|
1122
1703
|
async ({ path: notePath, frontmatter: updates, commit }) => {
|
|
1123
1704
|
try {
|
|
1124
|
-
const fullPath =
|
|
1705
|
+
const fullPath = path7.join(vaultPath2, notePath);
|
|
1125
1706
|
try {
|
|
1126
1707
|
await fs4.access(fullPath);
|
|
1127
1708
|
} catch {
|
|
@@ -1177,7 +1758,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
1177
1758
|
},
|
|
1178
1759
|
async ({ path: notePath, key, value, commit }) => {
|
|
1179
1760
|
try {
|
|
1180
|
-
const fullPath =
|
|
1761
|
+
const fullPath = path7.join(vaultPath2, notePath);
|
|
1181
1762
|
try {
|
|
1182
1763
|
await fs4.access(fullPath);
|
|
1183
1764
|
} catch {
|
|
@@ -1234,7 +1815,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
1234
1815
|
// src/tools/notes.ts
|
|
1235
1816
|
import { z as z4 } from "zod";
|
|
1236
1817
|
import fs5 from "fs/promises";
|
|
1237
|
-
import
|
|
1818
|
+
import path8 from "path";
|
|
1238
1819
|
function registerNoteTools(server2, vaultPath2) {
|
|
1239
1820
|
server2.tool(
|
|
1240
1821
|
"vault_create_note",
|
|
@@ -1256,7 +1837,7 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
1256
1837
|
};
|
|
1257
1838
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1258
1839
|
}
|
|
1259
|
-
const fullPath =
|
|
1840
|
+
const fullPath = path8.join(vaultPath2, notePath);
|
|
1260
1841
|
try {
|
|
1261
1842
|
await fs5.access(fullPath);
|
|
1262
1843
|
if (!overwrite) {
|
|
@@ -1269,7 +1850,7 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
1269
1850
|
}
|
|
1270
1851
|
} catch {
|
|
1271
1852
|
}
|
|
1272
|
-
const dir =
|
|
1853
|
+
const dir = path8.dirname(fullPath);
|
|
1273
1854
|
await fs5.mkdir(dir, { recursive: true });
|
|
1274
1855
|
await writeVaultFile(vaultPath2, notePath, content, frontmatter);
|
|
1275
1856
|
let gitCommit;
|
|
@@ -1328,7 +1909,7 @@ Content length: ${content.length} chars`,
|
|
|
1328
1909
|
};
|
|
1329
1910
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1330
1911
|
}
|
|
1331
|
-
const fullPath =
|
|
1912
|
+
const fullPath = path8.join(vaultPath2, notePath);
|
|
1332
1913
|
try {
|
|
1333
1914
|
await fs5.access(fullPath);
|
|
1334
1915
|
} catch {
|
|
@@ -1374,7 +1955,7 @@ Content length: ${content.length} chars`,
|
|
|
1374
1955
|
// src/tools/system.ts
|
|
1375
1956
|
import { z as z5 } from "zod";
|
|
1376
1957
|
import fs6 from "fs/promises";
|
|
1377
|
-
import
|
|
1958
|
+
import path9 from "path";
|
|
1378
1959
|
function registerSystemTools(server2, vaultPath2) {
|
|
1379
1960
|
server2.tool(
|
|
1380
1961
|
"vault_list_sections",
|
|
@@ -1394,7 +1975,7 @@ function registerSystemTools(server2, vaultPath2) {
|
|
|
1394
1975
|
};
|
|
1395
1976
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1396
1977
|
}
|
|
1397
|
-
const fullPath =
|
|
1978
|
+
const fullPath = path9.join(vaultPath2, notePath);
|
|
1398
1979
|
try {
|
|
1399
1980
|
await fs6.access(fullPath);
|
|
1400
1981
|
} catch {
|
|
@@ -1502,18 +2083,18 @@ Message: ${undoResult.undoneCommit.message}` : void 0
|
|
|
1502
2083
|
|
|
1503
2084
|
// src/core/vaultRoot.ts
|
|
1504
2085
|
import * as fs7 from "fs";
|
|
1505
|
-
import * as
|
|
2086
|
+
import * as path10 from "path";
|
|
1506
2087
|
var VAULT_MARKERS = [".obsidian", ".claude"];
|
|
1507
2088
|
function findVaultRoot(startPath) {
|
|
1508
|
-
let current =
|
|
2089
|
+
let current = path10.resolve(startPath || process.cwd());
|
|
1509
2090
|
while (true) {
|
|
1510
2091
|
for (const marker of VAULT_MARKERS) {
|
|
1511
|
-
const markerPath =
|
|
2092
|
+
const markerPath = path10.join(current, marker);
|
|
1512
2093
|
if (fs7.existsSync(markerPath) && fs7.statSync(markerPath).isDirectory()) {
|
|
1513
2094
|
return current;
|
|
1514
2095
|
}
|
|
1515
2096
|
}
|
|
1516
|
-
const parent =
|
|
2097
|
+
const parent = path10.dirname(current);
|
|
1517
2098
|
if (parent === current) {
|
|
1518
2099
|
return startPath || process.cwd();
|
|
1519
2100
|
}
|