@velvetmonkey/flywheel-crank 0.7.3 → 0.8.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.
Files changed (2) hide show
  1. package/dist/index.js +450 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -518,6 +518,7 @@ import path4 from "path";
518
518
 
519
519
  // src/core/stemmer.ts
520
520
  var STOPWORDS = /* @__PURE__ */ new Set([
521
+ // Articles, pronouns, prepositions (basic)
521
522
  "the",
522
523
  "a",
523
524
  "an",
@@ -618,11 +619,385 @@ var STOPWORDS = /* @__PURE__ */ new Set([
618
619
  "there",
619
620
  "any",
620
621
  "now",
621
- "new",
622
622
  "even",
623
623
  "much",
624
624
  "back",
625
- "going"
625
+ // Common verbs (critical for reducing false positives)
626
+ // These create matches like "Completed" → "Complete Guide"
627
+ "going",
628
+ "went",
629
+ "gone",
630
+ "come",
631
+ "came",
632
+ "coming",
633
+ "work",
634
+ "worked",
635
+ "working",
636
+ "works",
637
+ "make",
638
+ "made",
639
+ "making",
640
+ "makes",
641
+ "take",
642
+ "took",
643
+ "taken",
644
+ "taking",
645
+ "takes",
646
+ "give",
647
+ "gave",
648
+ "given",
649
+ "giving",
650
+ "gives",
651
+ "find",
652
+ "found",
653
+ "finding",
654
+ "finds",
655
+ "know",
656
+ "knew",
657
+ "known",
658
+ "knowing",
659
+ "knows",
660
+ "think",
661
+ "thought",
662
+ "thinking",
663
+ "thinks",
664
+ "look",
665
+ "looked",
666
+ "looking",
667
+ "looks",
668
+ "want",
669
+ "wanted",
670
+ "wanting",
671
+ "wants",
672
+ "tell",
673
+ "told",
674
+ "telling",
675
+ "tells",
676
+ "keep",
677
+ "kept",
678
+ "keeping",
679
+ "keeps",
680
+ "start",
681
+ "started",
682
+ "starting",
683
+ "starts",
684
+ "complete",
685
+ "completed",
686
+ "completing",
687
+ "completes",
688
+ "finish",
689
+ "finished",
690
+ "finishing",
691
+ "finishes",
692
+ "begin",
693
+ "began",
694
+ "begun",
695
+ "beginning",
696
+ "begins",
697
+ "end",
698
+ "ended",
699
+ "ending",
700
+ "ends",
701
+ "add",
702
+ "added",
703
+ "adding",
704
+ "adds",
705
+ "update",
706
+ "updated",
707
+ "updating",
708
+ "updates",
709
+ "change",
710
+ "changed",
711
+ "changing",
712
+ "changes",
713
+ "remove",
714
+ "removed",
715
+ "removing",
716
+ "removes",
717
+ "fix",
718
+ "fixed",
719
+ "fixing",
720
+ "fixes",
721
+ "create",
722
+ "created",
723
+ "creating",
724
+ "creates",
725
+ "build",
726
+ "built",
727
+ "building",
728
+ "builds",
729
+ "run",
730
+ "ran",
731
+ "running",
732
+ "runs",
733
+ "test",
734
+ "tested",
735
+ "testing",
736
+ "tests",
737
+ "release",
738
+ "released",
739
+ "releasing",
740
+ "releases",
741
+ "use",
742
+ "used",
743
+ "using",
744
+ "uses",
745
+ "get",
746
+ "got",
747
+ "gotten",
748
+ "getting",
749
+ "gets",
750
+ "set",
751
+ "setting",
752
+ "sets",
753
+ "put",
754
+ "putting",
755
+ "puts",
756
+ "try",
757
+ "tried",
758
+ "trying",
759
+ "tries",
760
+ "move",
761
+ "moved",
762
+ "moving",
763
+ "moves",
764
+ "show",
765
+ "showed",
766
+ "shown",
767
+ "showing",
768
+ "shows",
769
+ "help",
770
+ "helped",
771
+ "helping",
772
+ "helps",
773
+ "read",
774
+ "reading",
775
+ "reads",
776
+ "write",
777
+ "wrote",
778
+ "written",
779
+ "writing",
780
+ "writes",
781
+ "call",
782
+ "called",
783
+ "calling",
784
+ "calls",
785
+ "feel",
786
+ "felt",
787
+ "feeling",
788
+ "feels",
789
+ "seem",
790
+ "seemed",
791
+ "seeming",
792
+ "seems",
793
+ "turn",
794
+ "turned",
795
+ "turning",
796
+ "turns",
797
+ "leave",
798
+ "left",
799
+ "leaving",
800
+ "leaves",
801
+ "play",
802
+ "played",
803
+ "playing",
804
+ "plays",
805
+ "hold",
806
+ "held",
807
+ "holding",
808
+ "holds",
809
+ "bring",
810
+ "brought",
811
+ "bringing",
812
+ "brings",
813
+ "happen",
814
+ "happened",
815
+ "happening",
816
+ "happens",
817
+ "include",
818
+ "included",
819
+ "including",
820
+ "includes",
821
+ "continue",
822
+ "continued",
823
+ "continuing",
824
+ "continues",
825
+ "send",
826
+ "sent",
827
+ "sending",
828
+ "sends",
829
+ "receive",
830
+ "received",
831
+ "receiving",
832
+ "receives",
833
+ "follow",
834
+ "followed",
835
+ "following",
836
+ "follows",
837
+ "stop",
838
+ "stopped",
839
+ "stopping",
840
+ "stops",
841
+ "open",
842
+ "opened",
843
+ "opening",
844
+ "opens",
845
+ "close",
846
+ "closed",
847
+ "closing",
848
+ "closes",
849
+ "done",
850
+ "doing",
851
+ // Time words
852
+ "today",
853
+ "tomorrow",
854
+ "yesterday",
855
+ "daily",
856
+ "weekly",
857
+ "monthly",
858
+ "yearly",
859
+ "annually",
860
+ "morning",
861
+ "afternoon",
862
+ "evening",
863
+ "night",
864
+ "week",
865
+ "month",
866
+ "year",
867
+ "hour",
868
+ "minute",
869
+ "second",
870
+ "time",
871
+ "date",
872
+ "day",
873
+ "days",
874
+ "weeks",
875
+ "months",
876
+ "years",
877
+ "currently",
878
+ "recently",
879
+ "later",
880
+ "earlier",
881
+ "soon",
882
+ "always",
883
+ "never",
884
+ "sometimes",
885
+ "often",
886
+ "usually",
887
+ "rarely",
888
+ // Generic/filler words
889
+ "thing",
890
+ "things",
891
+ "stuff",
892
+ "something",
893
+ "anything",
894
+ "nothing",
895
+ "everything",
896
+ "someone",
897
+ "anyone",
898
+ "noone",
899
+ "everyone",
900
+ "somewhere",
901
+ "anywhere",
902
+ "nowhere",
903
+ "everywhere",
904
+ "good",
905
+ "better",
906
+ "best",
907
+ "great",
908
+ "nice",
909
+ "okay",
910
+ "fine",
911
+ "right",
912
+ "wrong",
913
+ "bad",
914
+ "worse",
915
+ "worst",
916
+ "lot",
917
+ "lots",
918
+ "many",
919
+ "several",
920
+ "various",
921
+ "different",
922
+ "similar",
923
+ "another",
924
+ "next",
925
+ "last",
926
+ "first",
927
+ "second",
928
+ "third",
929
+ "new",
930
+ "old",
931
+ "big",
932
+ "small",
933
+ "large",
934
+ "little",
935
+ "long",
936
+ "short",
937
+ "high",
938
+ "low",
939
+ "full",
940
+ "empty",
941
+ "whole",
942
+ "part",
943
+ "real",
944
+ "true",
945
+ "false",
946
+ "actual",
947
+ "main",
948
+ "important",
949
+ // Descriptive/qualifier words
950
+ "really",
951
+ "actually",
952
+ "basically",
953
+ "probably",
954
+ "definitely",
955
+ "certainly",
956
+ "possibly",
957
+ "maybe",
958
+ "perhaps",
959
+ "like",
960
+ "likely",
961
+ "unlikely",
962
+ "almost",
963
+ "nearly",
964
+ "quite",
965
+ "rather",
966
+ "pretty",
967
+ "still",
968
+ "already",
969
+ "yet",
970
+ "though",
971
+ "although",
972
+ "however",
973
+ "therefore",
974
+ "thus",
975
+ "hence",
976
+ "truly",
977
+ "simply",
978
+ "easily",
979
+ "quickly",
980
+ "slowly",
981
+ "well",
982
+ "just",
983
+ "ever",
984
+ "either",
985
+ "neither",
986
+ "whether",
987
+ "because",
988
+ "since",
989
+ "while",
990
+ "until",
991
+ "unless",
992
+ "except",
993
+ "besides",
994
+ "anyway",
995
+ "otherwise",
996
+ "instead",
997
+ "meanwhile",
998
+ "furthermore",
999
+ "moreover",
1000
+ "nevertheless"
626
1001
  ]);
627
1002
  function isConsonant(word, i) {
628
1003
  const c = word[i];
@@ -1152,36 +1527,88 @@ function isLikelyArticleTitle(name) {
1152
1527
  }
1153
1528
  return false;
1154
1529
  }
1155
- var MIN_SUGGESTION_SCORE = 5;
1156
- var MIN_MATCH_RATIO = 0.4;
1157
- function scoreEntity(entityName, contentTokens, contentStems) {
1530
+ var STRICTNESS_CONFIGS = {
1531
+ conservative: {
1532
+ minWordLength: 5,
1533
+ minSuggestionScore: 15,
1534
+ // Requires exact match (10) + at least one stem (5)
1535
+ minMatchRatio: 0.6,
1536
+ // 60% of multi-word entity must match
1537
+ requireMultipleMatches: true,
1538
+ // Single-word entities need multiple content matches
1539
+ stemMatchBonus: 3,
1540
+ // Lower bonus for stem-only matches
1541
+ exactMatchBonus: 10
1542
+ // Standard bonus for exact matches
1543
+ },
1544
+ balanced: {
1545
+ minWordLength: 4,
1546
+ minSuggestionScore: 8,
1547
+ // At least one exact match or two stem matches
1548
+ minMatchRatio: 0.4,
1549
+ // 40% of multi-word entity must match
1550
+ requireMultipleMatches: false,
1551
+ stemMatchBonus: 5,
1552
+ // Standard bonus for stem matches
1553
+ exactMatchBonus: 10
1554
+ // Standard bonus for exact matches
1555
+ },
1556
+ aggressive: {
1557
+ minWordLength: 4,
1558
+ minSuggestionScore: 5,
1559
+ // Single stem match is enough
1560
+ minMatchRatio: 0.3,
1561
+ // 30% of multi-word entity must match
1562
+ requireMultipleMatches: false,
1563
+ stemMatchBonus: 6,
1564
+ // Higher bonus for stem matches
1565
+ exactMatchBonus: 10
1566
+ // Standard bonus for exact matches
1567
+ }
1568
+ };
1569
+ var DEFAULT_STRICTNESS = "conservative";
1570
+ var MIN_SUGGESTION_SCORE = STRICTNESS_CONFIGS.balanced.minSuggestionScore;
1571
+ var MIN_MATCH_RATIO = STRICTNESS_CONFIGS.balanced.minMatchRatio;
1572
+ function scoreEntity(entityName, contentTokens, contentStems, config) {
1158
1573
  const entityTokens = tokenize(entityName);
1159
1574
  if (entityTokens.length === 0)
1160
1575
  return 0;
1161
1576
  const entityStems = entityTokens.map((t) => stem(t));
1162
1577
  let score = 0;
1163
1578
  let matchedWords = 0;
1579
+ let exactMatches = 0;
1164
1580
  for (let i = 0; i < entityTokens.length; i++) {
1165
1581
  const token = entityTokens[i];
1166
1582
  const entityStem = entityStems[i];
1167
1583
  if (contentTokens.has(token)) {
1168
- score += 10;
1584
+ score += config.exactMatchBonus;
1169
1585
  matchedWords++;
1586
+ exactMatches++;
1170
1587
  } else if (contentStems.has(entityStem)) {
1171
- score += 5;
1588
+ score += config.stemMatchBonus;
1172
1589
  matchedWords++;
1173
1590
  }
1174
1591
  }
1175
1592
  if (entityTokens.length > 1) {
1176
1593
  const matchRatio = matchedWords / entityTokens.length;
1177
- if (matchRatio < MIN_MATCH_RATIO) {
1594
+ if (matchRatio < config.minMatchRatio) {
1595
+ return 0;
1596
+ }
1597
+ }
1598
+ if (config.requireMultipleMatches && entityTokens.length === 1) {
1599
+ if (exactMatches === 0) {
1178
1600
  return 0;
1179
1601
  }
1180
1602
  }
1181
1603
  return score;
1182
1604
  }
1183
1605
  function suggestRelatedLinks(content, options = {}) {
1184
- const { maxSuggestions = 3, excludeLinked = true } = options;
1606
+ const {
1607
+ maxSuggestions = 3,
1608
+ excludeLinked = true,
1609
+ strictness = DEFAULT_STRICTNESS
1610
+ } = options;
1611
+ const config = STRICTNESS_CONFIGS[strictness];
1185
1612
  const emptyResult = { suggestions: [], suffix: "" };
1186
1613
  if (SUGGESTION_PATTERN.test(content)) {
1187
1614
  return emptyResult;
@@ -1213,11 +1640,11 @@ function suggestRelatedLinks(content, options = {}) {
1213
1640
  if (linkedEntities.has(entityName.toLowerCase())) {
1214
1641
  continue;
1215
1642
  }
1216
- const score = scoreEntity(entityName, contentTokens, contentStems);
1643
+ const score = scoreEntity(entityName, contentTokens, contentStems, config);
1217
1644
  if (score > 0) {
1218
1645
  directlyMatchedEntities.add(entityName);
1219
1646
  }
1220
- if (score >= MIN_SUGGESTION_SCORE) {
1647
+ if (score >= config.minSuggestionScore) {
1221
1648
  scoredEntities.push({ name: entityName, score });
1222
1649
  }
1223
1650
  }
@@ -1237,7 +1664,7 @@ function suggestRelatedLinks(content, options = {}) {
1237
1664
  const existing = scoredEntities.find((e) => e.name === entityName);
1238
1665
  if (existing) {
1239
1666
  existing.score += boost;
1240
- } else if (boost >= MIN_SUGGESTION_SCORE) {
1667
+ } else if (boost >= config.minSuggestionScore) {
1241
1668
  scoredEntities.push({ name: entityName, score: boost });
1242
1669
  }
1243
1670
  }
@@ -1271,9 +1698,10 @@ function registerMutationTools(server2, vaultPath2) {
1271
1698
  commit: z.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
1272
1699
  skipWikilinks: z.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
1273
1700
  preserveListNesting: z.boolean().default(true).describe("Detect and preserve the indentation level of surrounding list items. Set false to disable."),
1274
- suggestOutgoingLinks: z.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.')
1701
+ suggestOutgoingLinks: z.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.'),
1702
+ maxSuggestions: z.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)")
1275
1703
  },
1276
- async ({ path: notePath, section, content, position, format, commit, skipWikilinks, preserveListNesting, suggestOutgoingLinks }) => {
1704
+ async ({ path: notePath, section, content, position, format, commit, skipWikilinks, preserveListNesting, suggestOutgoingLinks, maxSuggestions }) => {
1277
1705
  try {
1278
1706
  const fullPath = path5.join(vaultPath2, notePath);
1279
1707
  try {
@@ -1299,7 +1727,7 @@ function registerMutationTools(server2, vaultPath2) {
1299
1727
  let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(content, skipWikilinks);
1300
1728
  let suggestInfo;
1301
1729
  if (suggestOutgoingLinks && !skipWikilinks) {
1302
- const result2 = suggestRelatedLinks(processedContent);
1730
+ const result2 = suggestRelatedLinks(processedContent, { maxSuggestions });
1303
1731
  if (result2.suffix) {
1304
1732
  processedContent = processedContent + " " + result2.suffix;
1305
1733
  suggestInfo = `Suggested: ${result2.suggestions.join(", ")}`;
@@ -1437,9 +1865,10 @@ function registerMutationTools(server2, vaultPath2) {
1437
1865
  useRegex: z.boolean().default(false).describe("Treat search as regex"),
1438
1866
  commit: z.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
1439
1867
  skipWikilinks: z.boolean().default(false).describe("If true, skip auto-wikilink application on replacement text"),
1440
- suggestOutgoingLinks: z.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.')
1868
+ suggestOutgoingLinks: z.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.'),
1869
+ maxSuggestions: z.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)")
1441
1870
  },
1442
- async ({ path: notePath, section, search, replacement, mode, useRegex, commit, skipWikilinks, suggestOutgoingLinks }) => {
1871
+ async ({ path: notePath, section, search, replacement, mode, useRegex, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions }) => {
1443
1872
  try {
1444
1873
  const fullPath = path5.join(vaultPath2, notePath);
1445
1874
  try {
@@ -1465,7 +1894,7 @@ function registerMutationTools(server2, vaultPath2) {
1465
1894
  let { content: processedReplacement, wikilinkInfo } = maybeApplyWikilinks(replacement, skipWikilinks);
1466
1895
  let suggestInfo;
1467
1896
  if (suggestOutgoingLinks && !skipWikilinks) {
1468
- const result2 = suggestRelatedLinks(processedReplacement);
1897
+ const result2 = suggestRelatedLinks(processedReplacement, { maxSuggestions });
1469
1898
  if (result2.suffix) {
1470
1899
  processedReplacement = processedReplacement + " " + result2.suffix;
1471
1900
  suggestInfo = `Suggested: ${result2.suggestions.join(", ")}`;
@@ -1674,9 +2103,10 @@ function registerTaskTools(server2, vaultPath2) {
1674
2103
  commit: z2.boolean().default(false).describe("If true, commit this change to git (creates undo point)"),
1675
2104
  skipWikilinks: z2.boolean().default(false).describe("If true, skip auto-wikilink application (wikilinks are applied by default)"),
1676
2105
  suggestOutgoingLinks: z2.boolean().default(true).describe('Append suggested outgoing wikilinks based on content (e.g., "\u2192 [[AI]] [[Philosophy]]"). Set false to disable.'),
2106
+ maxSuggestions: z2.number().min(1).max(10).default(3).describe("Maximum number of suggested wikilinks to append (1-10, default: 3)"),
1677
2107
  preserveListNesting: z2.boolean().default(true).describe("Preserve indentation when inserting into nested lists. Default: true")
1678
2108
  },
1679
- async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks, preserveListNesting }) => {
2109
+ async ({ path: notePath, section, task, position, completed, commit, skipWikilinks, suggestOutgoingLinks, maxSuggestions, preserveListNesting }) => {
1680
2110
  try {
1681
2111
  const fullPath = path6.join(vaultPath2, notePath);
1682
2112
  try {
@@ -1702,7 +2132,7 @@ function registerTaskTools(server2, vaultPath2) {
1702
2132
  let { content: processedTask, wikilinkInfo } = maybeApplyWikilinks(task.trim(), skipWikilinks);
1703
2133
  let suggestInfo;
1704
2134
  if (suggestOutgoingLinks && !skipWikilinks) {
1705
- const result2 = suggestRelatedLinks(processedTask);
2135
+ const result2 = suggestRelatedLinks(processedTask, { maxSuggestions });
1706
2136
  if (result2.suffix) {
1707
2137
  processedTask = processedTask + " " + result2.suffix;
1708
2138
  suggestInfo = `Suggested: ${result2.suggestions.join(", ")}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-crank",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Deterministic vault mutations for Obsidian via MCP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",