@velvetmonkey/flywheel-crank 0.5.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 +579 -121
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -492,10 +492,498 @@ import {
|
|
|
492
492
|
loadEntityCache,
|
|
493
493
|
saveEntityCache
|
|
494
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";
|
|
495
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
|
|
496
983
|
var entityIndex = null;
|
|
497
984
|
var indexReady = false;
|
|
498
985
|
var indexError = null;
|
|
986
|
+
var cooccurrenceIndex = null;
|
|
499
987
|
var DEFAULT_EXCLUDE_FOLDERS = [
|
|
500
988
|
"daily-notes",
|
|
501
989
|
"daily",
|
|
@@ -508,7 +996,7 @@ var DEFAULT_EXCLUDE_FOLDERS = [
|
|
|
508
996
|
"templates"
|
|
509
997
|
];
|
|
510
998
|
async function initializeEntityIndex(vaultPath2) {
|
|
511
|
-
const cacheFile =
|
|
999
|
+
const cacheFile = path4.join(vaultPath2, ".claude", "wikilink-entities.json");
|
|
512
1000
|
try {
|
|
513
1001
|
const cached = await loadEntityCache(cacheFile);
|
|
514
1002
|
if (cached) {
|
|
@@ -534,8 +1022,17 @@ async function rebuildIndex(vaultPath2, cacheFile) {
|
|
|
534
1022
|
excludeFolders: DEFAULT_EXCLUDE_FOLDERS
|
|
535
1023
|
});
|
|
536
1024
|
indexReady = true;
|
|
537
|
-
const
|
|
538
|
-
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
|
+
}
|
|
539
1036
|
try {
|
|
540
1037
|
await saveEntityCache(cacheFile, entityIndex);
|
|
541
1038
|
console.error(`[Crank] Entity cache saved`);
|
|
@@ -579,86 +1076,6 @@ function maybeApplyWikilinks(content, skipWikilinks) {
|
|
|
579
1076
|
return { content: result.content };
|
|
580
1077
|
}
|
|
581
1078
|
var SUGGESTION_PATTERN = /→\s*\[\[.+$/;
|
|
582
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
583
|
-
"the",
|
|
584
|
-
"a",
|
|
585
|
-
"an",
|
|
586
|
-
"and",
|
|
587
|
-
"or",
|
|
588
|
-
"but",
|
|
589
|
-
"in",
|
|
590
|
-
"on",
|
|
591
|
-
"at",
|
|
592
|
-
"to",
|
|
593
|
-
"for",
|
|
594
|
-
"of",
|
|
595
|
-
"with",
|
|
596
|
-
"by",
|
|
597
|
-
"from",
|
|
598
|
-
"as",
|
|
599
|
-
"is",
|
|
600
|
-
"was",
|
|
601
|
-
"are",
|
|
602
|
-
"were",
|
|
603
|
-
"been",
|
|
604
|
-
"be",
|
|
605
|
-
"have",
|
|
606
|
-
"has",
|
|
607
|
-
"had",
|
|
608
|
-
"do",
|
|
609
|
-
"does",
|
|
610
|
-
"did",
|
|
611
|
-
"will",
|
|
612
|
-
"would",
|
|
613
|
-
"could",
|
|
614
|
-
"should",
|
|
615
|
-
"may",
|
|
616
|
-
"might",
|
|
617
|
-
"must",
|
|
618
|
-
"shall",
|
|
619
|
-
"can",
|
|
620
|
-
"need",
|
|
621
|
-
"this",
|
|
622
|
-
"that",
|
|
623
|
-
"these",
|
|
624
|
-
"those",
|
|
625
|
-
"i",
|
|
626
|
-
"you",
|
|
627
|
-
"he",
|
|
628
|
-
"she",
|
|
629
|
-
"it",
|
|
630
|
-
"we",
|
|
631
|
-
"they",
|
|
632
|
-
"what",
|
|
633
|
-
"which",
|
|
634
|
-
"who",
|
|
635
|
-
"whom",
|
|
636
|
-
"when",
|
|
637
|
-
"where",
|
|
638
|
-
"why",
|
|
639
|
-
"how",
|
|
640
|
-
"all",
|
|
641
|
-
"each",
|
|
642
|
-
"every",
|
|
643
|
-
"both",
|
|
644
|
-
"few",
|
|
645
|
-
"more",
|
|
646
|
-
"most",
|
|
647
|
-
"other",
|
|
648
|
-
"some",
|
|
649
|
-
"such",
|
|
650
|
-
"no",
|
|
651
|
-
"not",
|
|
652
|
-
"only",
|
|
653
|
-
"own",
|
|
654
|
-
"same",
|
|
655
|
-
"so",
|
|
656
|
-
"than",
|
|
657
|
-
"too",
|
|
658
|
-
"very",
|
|
659
|
-
"just",
|
|
660
|
-
"also"
|
|
661
|
-
]);
|
|
662
1079
|
function extractLinkedEntities(content) {
|
|
663
1080
|
const linked = /* @__PURE__ */ new Set();
|
|
664
1081
|
const wikilinkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
@@ -668,23 +1085,37 @@ function extractLinkedEntities(content) {
|
|
|
668
1085
|
}
|
|
669
1086
|
return linked;
|
|
670
1087
|
}
|
|
671
|
-
function
|
|
672
|
-
const
|
|
673
|
-
const
|
|
674
|
-
|
|
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 };
|
|
675
1093
|
}
|
|
676
|
-
|
|
677
|
-
|
|
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));
|
|
678
1102
|
let score = 0;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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;
|
|
688
1119
|
}
|
|
689
1120
|
}
|
|
690
1121
|
return score;
|
|
@@ -702,24 +1133,51 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
702
1133
|
if (entities.length === 0) {
|
|
703
1134
|
return emptyResult;
|
|
704
1135
|
}
|
|
705
|
-
const contentTokens =
|
|
706
|
-
if (contentTokens.
|
|
1136
|
+
const { tokens: contentTokens, stems: contentStems } = tokenizeForMatching(content);
|
|
1137
|
+
if (contentTokens.size === 0) {
|
|
707
1138
|
return emptyResult;
|
|
708
1139
|
}
|
|
709
1140
|
const linkedEntities = excludeLinked ? extractLinkedEntities(content) : /* @__PURE__ */ new Set();
|
|
710
1141
|
const scoredEntities = [];
|
|
1142
|
+
const directlyMatchedEntities = /* @__PURE__ */ new Set();
|
|
711
1143
|
for (const entity of entities) {
|
|
712
1144
|
const entityName = typeof entity === "string" ? entity : entity.name;
|
|
713
1145
|
if (!entityName)
|
|
714
1146
|
continue;
|
|
1147
|
+
if (entityName.length > MAX_ENTITY_LENGTH) {
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
715
1150
|
if (linkedEntities.has(entityName.toLowerCase())) {
|
|
716
1151
|
continue;
|
|
717
1152
|
}
|
|
718
|
-
const score = scoreEntity(entityName, contentTokens);
|
|
1153
|
+
const score = scoreEntity(entityName, contentTokens, contentStems);
|
|
719
1154
|
if (score > 0) {
|
|
1155
|
+
directlyMatchedEntities.add(entityName);
|
|
1156
|
+
}
|
|
1157
|
+
if (score >= MIN_SUGGESTION_SCORE) {
|
|
720
1158
|
scoredEntities.push({ name: entityName, score });
|
|
721
1159
|
}
|
|
722
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
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
723
1181
|
scoredEntities.sort((a, b) => b.score - a.score);
|
|
724
1182
|
const topSuggestions = scoredEntities.slice(0, maxSuggestions).map((e) => e.name);
|
|
725
1183
|
if (topSuggestions.length === 0) {
|
|
@@ -734,7 +1192,7 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
734
1192
|
|
|
735
1193
|
// src/tools/mutations.ts
|
|
736
1194
|
import fs2 from "fs/promises";
|
|
737
|
-
import
|
|
1195
|
+
import path5 from "path";
|
|
738
1196
|
function registerMutationTools(server2, vaultPath2) {
|
|
739
1197
|
server2.tool(
|
|
740
1198
|
"vault_add_to_section",
|
|
@@ -752,7 +1210,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
752
1210
|
},
|
|
753
1211
|
async ({ path: notePath, section, content, position, format, commit, skipWikilinks, preserveListNesting, suggestOutgoingLinks }) => {
|
|
754
1212
|
try {
|
|
755
|
-
const fullPath =
|
|
1213
|
+
const fullPath = path5.join(vaultPath2, notePath);
|
|
756
1214
|
try {
|
|
757
1215
|
await fs2.access(fullPath);
|
|
758
1216
|
} catch {
|
|
@@ -836,7 +1294,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
836
1294
|
},
|
|
837
1295
|
async ({ path: notePath, section, pattern, mode, useRegex, commit }) => {
|
|
838
1296
|
try {
|
|
839
|
-
const fullPath =
|
|
1297
|
+
const fullPath = path5.join(vaultPath2, notePath);
|
|
840
1298
|
try {
|
|
841
1299
|
await fs2.access(fullPath);
|
|
842
1300
|
} catch {
|
|
@@ -918,7 +1376,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
918
1376
|
},
|
|
919
1377
|
async ({ path: notePath, section, search, replacement, mode, useRegex, commit, skipWikilinks, suggestOutgoingLinks }) => {
|
|
920
1378
|
try {
|
|
921
|
-
const fullPath =
|
|
1379
|
+
const fullPath = path5.join(vaultPath2, notePath);
|
|
922
1380
|
try {
|
|
923
1381
|
await fs2.access(fullPath);
|
|
924
1382
|
} catch {
|
|
@@ -1004,7 +1462,7 @@ function registerMutationTools(server2, vaultPath2) {
|
|
|
1004
1462
|
// src/tools/tasks.ts
|
|
1005
1463
|
import { z as z2 } from "zod";
|
|
1006
1464
|
import fs3 from "fs/promises";
|
|
1007
|
-
import
|
|
1465
|
+
import path6 from "path";
|
|
1008
1466
|
var TASK_REGEX = /^(\s*)-\s*\[([ xX])\]\s*(.*)$/;
|
|
1009
1467
|
function findTasks(content, section) {
|
|
1010
1468
|
const lines = content.split("\n");
|
|
@@ -1060,7 +1518,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
1060
1518
|
},
|
|
1061
1519
|
async ({ path: notePath, task, section, commit }) => {
|
|
1062
1520
|
try {
|
|
1063
|
-
const fullPath =
|
|
1521
|
+
const fullPath = path6.join(vaultPath2, notePath);
|
|
1064
1522
|
try {
|
|
1065
1523
|
await fs3.access(fullPath);
|
|
1066
1524
|
} catch {
|
|
@@ -1155,7 +1613,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
1155
1613
|
},
|
|
1156
1614
|
async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks, preserveListNesting }) => {
|
|
1157
1615
|
try {
|
|
1158
|
-
const fullPath =
|
|
1616
|
+
const fullPath = path6.join(vaultPath2, notePath);
|
|
1159
1617
|
try {
|
|
1160
1618
|
await fs3.access(fullPath);
|
|
1161
1619
|
} catch {
|
|
@@ -1232,7 +1690,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
1232
1690
|
// src/tools/frontmatter.ts
|
|
1233
1691
|
import { z as z3 } from "zod";
|
|
1234
1692
|
import fs4 from "fs/promises";
|
|
1235
|
-
import
|
|
1693
|
+
import path7 from "path";
|
|
1236
1694
|
function registerFrontmatterTools(server2, vaultPath2) {
|
|
1237
1695
|
server2.tool(
|
|
1238
1696
|
"vault_update_frontmatter",
|
|
@@ -1244,7 +1702,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
1244
1702
|
},
|
|
1245
1703
|
async ({ path: notePath, frontmatter: updates, commit }) => {
|
|
1246
1704
|
try {
|
|
1247
|
-
const fullPath =
|
|
1705
|
+
const fullPath = path7.join(vaultPath2, notePath);
|
|
1248
1706
|
try {
|
|
1249
1707
|
await fs4.access(fullPath);
|
|
1250
1708
|
} catch {
|
|
@@ -1300,7 +1758,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
1300
1758
|
},
|
|
1301
1759
|
async ({ path: notePath, key, value, commit }) => {
|
|
1302
1760
|
try {
|
|
1303
|
-
const fullPath =
|
|
1761
|
+
const fullPath = path7.join(vaultPath2, notePath);
|
|
1304
1762
|
try {
|
|
1305
1763
|
await fs4.access(fullPath);
|
|
1306
1764
|
} catch {
|
|
@@ -1357,7 +1815,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
1357
1815
|
// src/tools/notes.ts
|
|
1358
1816
|
import { z as z4 } from "zod";
|
|
1359
1817
|
import fs5 from "fs/promises";
|
|
1360
|
-
import
|
|
1818
|
+
import path8 from "path";
|
|
1361
1819
|
function registerNoteTools(server2, vaultPath2) {
|
|
1362
1820
|
server2.tool(
|
|
1363
1821
|
"vault_create_note",
|
|
@@ -1379,7 +1837,7 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
1379
1837
|
};
|
|
1380
1838
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1381
1839
|
}
|
|
1382
|
-
const fullPath =
|
|
1840
|
+
const fullPath = path8.join(vaultPath2, notePath);
|
|
1383
1841
|
try {
|
|
1384
1842
|
await fs5.access(fullPath);
|
|
1385
1843
|
if (!overwrite) {
|
|
@@ -1392,7 +1850,7 @@ function registerNoteTools(server2, vaultPath2) {
|
|
|
1392
1850
|
}
|
|
1393
1851
|
} catch {
|
|
1394
1852
|
}
|
|
1395
|
-
const dir =
|
|
1853
|
+
const dir = path8.dirname(fullPath);
|
|
1396
1854
|
await fs5.mkdir(dir, { recursive: true });
|
|
1397
1855
|
await writeVaultFile(vaultPath2, notePath, content, frontmatter);
|
|
1398
1856
|
let gitCommit;
|
|
@@ -1451,7 +1909,7 @@ Content length: ${content.length} chars`,
|
|
|
1451
1909
|
};
|
|
1452
1910
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1453
1911
|
}
|
|
1454
|
-
const fullPath =
|
|
1912
|
+
const fullPath = path8.join(vaultPath2, notePath);
|
|
1455
1913
|
try {
|
|
1456
1914
|
await fs5.access(fullPath);
|
|
1457
1915
|
} catch {
|
|
@@ -1497,7 +1955,7 @@ Content length: ${content.length} chars`,
|
|
|
1497
1955
|
// src/tools/system.ts
|
|
1498
1956
|
import { z as z5 } from "zod";
|
|
1499
1957
|
import fs6 from "fs/promises";
|
|
1500
|
-
import
|
|
1958
|
+
import path9 from "path";
|
|
1501
1959
|
function registerSystemTools(server2, vaultPath2) {
|
|
1502
1960
|
server2.tool(
|
|
1503
1961
|
"vault_list_sections",
|
|
@@ -1517,7 +1975,7 @@ function registerSystemTools(server2, vaultPath2) {
|
|
|
1517
1975
|
};
|
|
1518
1976
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
1519
1977
|
}
|
|
1520
|
-
const fullPath =
|
|
1978
|
+
const fullPath = path9.join(vaultPath2, notePath);
|
|
1521
1979
|
try {
|
|
1522
1980
|
await fs6.access(fullPath);
|
|
1523
1981
|
} catch {
|
|
@@ -1625,18 +2083,18 @@ Message: ${undoResult.undoneCommit.message}` : void 0
|
|
|
1625
2083
|
|
|
1626
2084
|
// src/core/vaultRoot.ts
|
|
1627
2085
|
import * as fs7 from "fs";
|
|
1628
|
-
import * as
|
|
2086
|
+
import * as path10 from "path";
|
|
1629
2087
|
var VAULT_MARKERS = [".obsidian", ".claude"];
|
|
1630
2088
|
function findVaultRoot(startPath) {
|
|
1631
|
-
let current =
|
|
2089
|
+
let current = path10.resolve(startPath || process.cwd());
|
|
1632
2090
|
while (true) {
|
|
1633
2091
|
for (const marker of VAULT_MARKERS) {
|
|
1634
|
-
const markerPath =
|
|
2092
|
+
const markerPath = path10.join(current, marker);
|
|
1635
2093
|
if (fs7.existsSync(markerPath) && fs7.statSync(markerPath).isDirectory()) {
|
|
1636
2094
|
return current;
|
|
1637
2095
|
}
|
|
1638
2096
|
}
|
|
1639
|
-
const parent =
|
|
2097
|
+
const parent = path10.dirname(current);
|
|
1640
2098
|
if (parent === current) {
|
|
1641
2099
|
return startPath || process.cwd();
|
|
1642
2100
|
}
|