@velvetmonkey/flywheel-crank 0.8.1 → 0.9.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 +120 -40
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -14,6 +14,10 @@ import matter from "gray-matter";
14
14
 
15
15
  // src/core/constants.ts
16
16
  var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
17
+ function estimateTokens(content) {
18
+ const str = typeof content === "string" ? content : JSON.stringify(content);
19
+ return Math.ceil(str.length / 4);
20
+ }
17
21
 
18
22
  // src/core/writer.ts
19
23
  var SENSITIVE_PATH_PATTERNS = [
@@ -510,9 +514,12 @@ async function undoLastCommit(vaultPath2) {
510
514
  import {
511
515
  scanVaultEntities,
512
516
  getAllEntities,
517
+ getEntityName,
518
+ getEntityAliases,
513
519
  applyWikilinks,
514
520
  loadEntityCache,
515
- saveEntityCache
521
+ saveEntityCache,
522
+ ENTITY_CACHE_VERSION
516
523
  } from "@velvetmonkey/vault-core";
517
524
  import path4 from "path";
518
525
 
@@ -1411,6 +1418,12 @@ async function initializeEntityIndex(vaultPath2) {
1411
1418
  try {
1412
1419
  const cached = await loadEntityCache(cacheFile);
1413
1420
  if (cached) {
1421
+ const cacheVersion = cached._metadata.version ?? 1;
1422
+ if (cacheVersion < ENTITY_CACHE_VERSION) {
1423
+ console.error(`[Crank] Cache version ${cacheVersion} < ${ENTITY_CACHE_VERSION}, rebuilding...`);
1424
+ await rebuildIndex(vaultPath2, cacheFile);
1425
+ return;
1426
+ }
1414
1427
  entityIndex = cached;
1415
1428
  indexReady = true;
1416
1429
  console.error(`[Crank] Loaded ${cached._metadata.total_entities} entities from cache`);
@@ -1569,33 +1582,51 @@ var STRICTNESS_CONFIGS = {
1569
1582
  var DEFAULT_STRICTNESS = "conservative";
1570
1583
  var MIN_SUGGESTION_SCORE = STRICTNESS_CONFIGS.balanced.minSuggestionScore;
1571
1584
  var MIN_MATCH_RATIO = STRICTNESS_CONFIGS.balanced.minMatchRatio;
1572
- function scoreEntity(entityName, contentTokens, contentStems, config) {
1573
- const entityTokens = tokenize(entityName);
1574
- if (entityTokens.length === 0)
1575
- return 0;
1576
- const entityStems = entityTokens.map((t) => stem(t));
1585
+ function scoreNameAgainstContent(name, contentTokens, contentStems, config) {
1586
+ const nameTokens = tokenize(name);
1587
+ if (nameTokens.length === 0) {
1588
+ return { score: 0, matchedWords: 0, exactMatches: 0, totalTokens: 0 };
1589
+ }
1590
+ const nameStems = nameTokens.map((t) => stem(t));
1577
1591
  let score = 0;
1578
1592
  let matchedWords = 0;
1579
1593
  let exactMatches = 0;
1580
- for (let i = 0; i < entityTokens.length; i++) {
1581
- const token = entityTokens[i];
1582
- const entityStem = entityStems[i];
1594
+ for (let i = 0; i < nameTokens.length; i++) {
1595
+ const token = nameTokens[i];
1596
+ const nameStem = nameStems[i];
1583
1597
  if (contentTokens.has(token)) {
1584
1598
  score += config.exactMatchBonus;
1585
1599
  matchedWords++;
1586
1600
  exactMatches++;
1587
- } else if (contentStems.has(entityStem)) {
1601
+ } else if (contentStems.has(nameStem)) {
1588
1602
  score += config.stemMatchBonus;
1589
1603
  matchedWords++;
1590
1604
  }
1591
1605
  }
1592
- if (entityTokens.length > 1) {
1593
- const matchRatio = matchedWords / entityTokens.length;
1606
+ return { score, matchedWords, exactMatches, totalTokens: nameTokens.length };
1607
+ }
1608
+ function scoreEntity(entity, contentTokens, contentStems, config) {
1609
+ const entityName = getEntityName(entity);
1610
+ const aliases = getEntityAliases(entity);
1611
+ const nameResult = scoreNameAgainstContent(entityName, contentTokens, contentStems, config);
1612
+ let bestAliasResult = { score: 0, matchedWords: 0, exactMatches: 0, totalTokens: 0 };
1613
+ for (const alias of aliases) {
1614
+ const aliasResult = scoreNameAgainstContent(alias, contentTokens, contentStems, config);
1615
+ if (aliasResult.score > bestAliasResult.score) {
1616
+ bestAliasResult = aliasResult;
1617
+ }
1618
+ }
1619
+ const bestResult = nameResult.score >= bestAliasResult.score ? nameResult : bestAliasResult;
1620
+ const { score, matchedWords, exactMatches, totalTokens } = bestResult;
1621
+ if (totalTokens === 0)
1622
+ return 0;
1623
+ if (totalTokens > 1) {
1624
+ const matchRatio = matchedWords / totalTokens;
1594
1625
  if (matchRatio < config.minMatchRatio) {
1595
1626
  return 0;
1596
1627
  }
1597
1628
  }
1598
- if (config.requireMultipleMatches && entityTokens.length === 1) {
1629
+ if (config.requireMultipleMatches && totalTokens === 1) {
1599
1630
  if (exactMatches === 0) {
1600
1631
  return 0;
1601
1632
  }
@@ -1628,7 +1659,7 @@ function suggestRelatedLinks(content, options = {}) {
1628
1659
  const scoredEntities = [];
1629
1660
  const directlyMatchedEntities = /* @__PURE__ */ new Set();
1630
1661
  for (const entity of entities) {
1631
- const entityName = typeof entity === "string" ? entity : entity.name;
1662
+ const entityName = getEntityName(entity);
1632
1663
  if (!entityName)
1633
1664
  continue;
1634
1665
  if (entityName.length > MAX_ENTITY_LENGTH) {
@@ -1640,7 +1671,7 @@ function suggestRelatedLinks(content, options = {}) {
1640
1671
  if (linkedEntities.has(entityName.toLowerCase())) {
1641
1672
  continue;
1642
1673
  }
1643
- const score = scoreEntity(entityName, contentTokens, contentStems, config);
1674
+ const score = scoreEntity(entity, contentTokens, contentStems, config);
1644
1675
  if (score > 0) {
1645
1676
  directlyMatchedEntities.add(entityName);
1646
1677
  }
@@ -1650,7 +1681,7 @@ function suggestRelatedLinks(content, options = {}) {
1650
1681
  }
1651
1682
  if (cooccurrenceIndex && directlyMatchedEntities.size > 0) {
1652
1683
  for (const entity of entities) {
1653
- const entityName = typeof entity === "string" ? entity : entity.name;
1684
+ const entityName = getEntityName(entity);
1654
1685
  if (!entityName)
1655
1686
  continue;
1656
1687
  if (entityName.length > MAX_ENTITY_LENGTH)
@@ -1710,8 +1741,10 @@ function registerMutationTools(server2, vaultPath2) {
1710
1741
  const result2 = {
1711
1742
  success: false,
1712
1743
  message: `File not found: ${notePath}`,
1713
- path: notePath
1744
+ path: notePath,
1745
+ tokensEstimate: 0
1714
1746
  };
1747
+ result2.tokensEstimate = estimateTokens(result2);
1715
1748
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1716
1749
  }
1717
1750
  const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
@@ -1720,8 +1753,10 @@ function registerMutationTools(server2, vaultPath2) {
1720
1753
  const result2 = {
1721
1754
  success: false,
1722
1755
  message: `Section not found: ${section}`,
1723
- path: notePath
1756
+ path: notePath,
1757
+ tokensEstimate: 0
1724
1758
  };
1759
+ result2.tokensEstimate = estimateTokens(result2);
1725
1760
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1726
1761
  }
1727
1762
  let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(content, skipWikilinks);
@@ -1761,15 +1796,20 @@ function registerMutationTools(server2, vaultPath2) {
1761
1796
  path: notePath,
1762
1797
  preview,
1763
1798
  gitCommit,
1764
- gitError
1799
+ gitError,
1800
+ tokensEstimate: 0
1801
+ // Will be set below
1765
1802
  };
1803
+ result.tokensEstimate = estimateTokens(result);
1766
1804
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1767
1805
  } catch (error) {
1768
1806
  const result = {
1769
1807
  success: false,
1770
1808
  message: `Failed to add content: ${error instanceof Error ? error.message : String(error)}`,
1771
- path: notePath
1809
+ path: notePath,
1810
+ tokensEstimate: 0
1772
1811
  };
1812
+ result.tokensEstimate = estimateTokens(result);
1773
1813
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1774
1814
  }
1775
1815
  }
@@ -1794,8 +1834,10 @@ function registerMutationTools(server2, vaultPath2) {
1794
1834
  const result2 = {
1795
1835
  success: false,
1796
1836
  message: `File not found: ${notePath}`,
1797
- path: notePath
1837
+ path: notePath,
1838
+ tokensEstimate: 0
1798
1839
  };
1840
+ result2.tokensEstimate = estimateTokens(result2);
1799
1841
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1800
1842
  }
1801
1843
  const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
@@ -1804,8 +1846,10 @@ function registerMutationTools(server2, vaultPath2) {
1804
1846
  const result2 = {
1805
1847
  success: false,
1806
1848
  message: `Section not found: ${section}`,
1807
- path: notePath
1849
+ path: notePath,
1850
+ tokensEstimate: 0
1808
1851
  };
1852
+ result2.tokensEstimate = estimateTokens(result2);
1809
1853
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1810
1854
  }
1811
1855
  const removeResult = removeFromSection(
@@ -1819,8 +1863,10 @@ function registerMutationTools(server2, vaultPath2) {
1819
1863
  const result2 = {
1820
1864
  success: false,
1821
1865
  message: `No content matching "${pattern}" found in section "${sectionBoundary.name}"`,
1822
- path: notePath
1866
+ path: notePath,
1867
+ tokensEstimate: 0
1823
1868
  };
1869
+ result2.tokensEstimate = estimateTokens(result2);
1824
1870
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1825
1871
  }
1826
1872
  await writeVaultFile(vaultPath2, notePath, removeResult.content, frontmatter);
@@ -1840,15 +1886,19 @@ function registerMutationTools(server2, vaultPath2) {
1840
1886
  path: notePath,
1841
1887
  preview: removeResult.removedLines.join("\n"),
1842
1888
  gitCommit,
1843
- gitError
1889
+ gitError,
1890
+ tokensEstimate: 0
1844
1891
  };
1892
+ result.tokensEstimate = estimateTokens(result);
1845
1893
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1846
1894
  } catch (error) {
1847
1895
  const result = {
1848
1896
  success: false,
1849
1897
  message: `Failed to remove content: ${error instanceof Error ? error.message : String(error)}`,
1850
- path: notePath
1898
+ path: notePath,
1899
+ tokensEstimate: 0
1851
1900
  };
1901
+ result.tokensEstimate = estimateTokens(result);
1852
1902
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1853
1903
  }
1854
1904
  }
@@ -1877,8 +1927,10 @@ function registerMutationTools(server2, vaultPath2) {
1877
1927
  const result2 = {
1878
1928
  success: false,
1879
1929
  message: `File not found: ${notePath}`,
1880
- path: notePath
1930
+ path: notePath,
1931
+ tokensEstimate: 0
1881
1932
  };
1933
+ result2.tokensEstimate = estimateTokens(result2);
1882
1934
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1883
1935
  }
1884
1936
  const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
@@ -1887,8 +1939,10 @@ function registerMutationTools(server2, vaultPath2) {
1887
1939
  const result2 = {
1888
1940
  success: false,
1889
1941
  message: `Section not found: ${section}`,
1890
- path: notePath
1942
+ path: notePath,
1943
+ tokensEstimate: 0
1891
1944
  };
1945
+ result2.tokensEstimate = estimateTokens(result2);
1892
1946
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1893
1947
  }
1894
1948
  let { content: processedReplacement, wikilinkInfo } = maybeApplyWikilinks(replacement, skipWikilinks);
@@ -1912,8 +1966,10 @@ function registerMutationTools(server2, vaultPath2) {
1912
1966
  const result2 = {
1913
1967
  success: false,
1914
1968
  message: `No content matching "${search}" found in section "${sectionBoundary.name}"`,
1915
- path: notePath
1969
+ path: notePath,
1970
+ tokensEstimate: 0
1916
1971
  };
1972
+ result2.tokensEstimate = estimateTokens(result2);
1917
1973
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1918
1974
  }
1919
1975
  await writeVaultFile(vaultPath2, notePath, replaceResult.content, frontmatter);
@@ -1937,15 +1993,19 @@ function registerMutationTools(server2, vaultPath2) {
1937
1993
  path: notePath,
1938
1994
  preview: previewLines.join("\n"),
1939
1995
  gitCommit,
1940
- gitError
1996
+ gitError,
1997
+ tokensEstimate: 0
1941
1998
  };
1999
+ result.tokensEstimate = estimateTokens(result);
1942
2000
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1943
2001
  } catch (error) {
1944
2002
  const result = {
1945
2003
  success: false,
1946
2004
  message: `Failed to replace content: ${error instanceof Error ? error.message : String(error)}`,
1947
- path: notePath
2005
+ path: notePath,
2006
+ tokensEstimate: 0
1948
2007
  };
2008
+ result.tokensEstimate = estimateTokens(result);
1949
2009
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1950
2010
  }
1951
2011
  }
@@ -2019,8 +2079,10 @@ function registerTaskTools(server2, vaultPath2) {
2019
2079
  const result2 = {
2020
2080
  success: false,
2021
2081
  message: `File not found: ${notePath}`,
2022
- path: notePath
2082
+ path: notePath,
2083
+ tokensEstimate: 0
2023
2084
  };
2085
+ result2.tokensEstimate = estimateTokens(result2);
2024
2086
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
2025
2087
  }
2026
2088
  const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
@@ -2031,8 +2093,10 @@ function registerTaskTools(server2, vaultPath2) {
2031
2093
  const result2 = {
2032
2094
  success: false,
2033
2095
  message: `Section not found: ${section}`,
2034
- path: notePath
2096
+ path: notePath,
2097
+ tokensEstimate: 0
2035
2098
  };
2099
+ result2.tokensEstimate = estimateTokens(result2);
2036
2100
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
2037
2101
  }
2038
2102
  sectionBoundary = found;
@@ -2046,8 +2110,10 @@ function registerTaskTools(server2, vaultPath2) {
2046
2110
  const result2 = {
2047
2111
  success: false,
2048
2112
  message: `No task found matching "${task}"${section ? ` in section "${section}"` : ""}`,
2049
- path: notePath
2113
+ path: notePath,
2114
+ tokensEstimate: 0
2050
2115
  };
2116
+ result2.tokensEstimate = estimateTokens(result2);
2051
2117
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
2052
2118
  }
2053
2119
  const toggleResult = toggleTask(fileContent, matchingTask.line);
@@ -2055,8 +2121,10 @@ function registerTaskTools(server2, vaultPath2) {
2055
2121
  const result2 = {
2056
2122
  success: false,
2057
2123
  message: "Failed to toggle task",
2058
- path: notePath
2124
+ path: notePath,
2125
+ tokensEstimate: 0
2059
2126
  };
2127
+ result2.tokensEstimate = estimateTokens(result2);
2060
2128
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
2061
2129
  }
2062
2130
  await writeVaultFile(vaultPath2, notePath, toggleResult.content, frontmatter);
@@ -2078,15 +2146,19 @@ function registerTaskTools(server2, vaultPath2) {
2078
2146
  path: notePath,
2079
2147
  preview: `${checkbox} ${matchingTask.text}`,
2080
2148
  gitCommit,
2081
- gitError
2149
+ gitError,
2150
+ tokensEstimate: 0
2082
2151
  };
2152
+ result.tokensEstimate = estimateTokens(result);
2083
2153
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2084
2154
  } catch (error) {
2085
2155
  const result = {
2086
2156
  success: false,
2087
2157
  message: `Failed to toggle task: ${error instanceof Error ? error.message : String(error)}`,
2088
- path: notePath
2158
+ path: notePath,
2159
+ tokensEstimate: 0
2089
2160
  };
2161
+ result.tokensEstimate = estimateTokens(result);
2090
2162
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2091
2163
  }
2092
2164
  }
@@ -2115,8 +2187,10 @@ function registerTaskTools(server2, vaultPath2) {
2115
2187
  const result2 = {
2116
2188
  success: false,
2117
2189
  message: `File not found: ${notePath}`,
2118
- path: notePath
2190
+ path: notePath,
2191
+ tokensEstimate: 0
2119
2192
  };
2193
+ result2.tokensEstimate = estimateTokens(result2);
2120
2194
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
2121
2195
  }
2122
2196
  const { content: fileContent, frontmatter } = await readVaultFile(vaultPath2, notePath);
@@ -2125,8 +2199,10 @@ function registerTaskTools(server2, vaultPath2) {
2125
2199
  const result2 = {
2126
2200
  success: false,
2127
2201
  message: `Section not found: ${section}`,
2128
- path: notePath
2202
+ path: notePath,
2203
+ tokensEstimate: 0
2129
2204
  };
2205
+ result2.tokensEstimate = estimateTokens(result2);
2130
2206
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
2131
2207
  }
2132
2208
  let { content: processedTask, wikilinkInfo } = maybeApplyWikilinks(task.trim(), skipWikilinks);
@@ -2166,15 +2242,19 @@ function registerTaskTools(server2, vaultPath2) {
2166
2242
  preview: taskLine + (infoLines.length > 0 ? `
2167
2243
  (${infoLines.join("; ")})` : ""),
2168
2244
  gitCommit,
2169
- gitError
2245
+ gitError,
2246
+ tokensEstimate: 0
2170
2247
  };
2248
+ result.tokensEstimate = estimateTokens(result);
2171
2249
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2172
2250
  } catch (error) {
2173
2251
  const result = {
2174
2252
  success: false,
2175
2253
  message: `Failed to add task: ${error instanceof Error ? error.message : String(error)}`,
2176
- path: notePath
2254
+ path: notePath,
2255
+ tokensEstimate: 0
2177
2256
  };
2257
+ result.tokensEstimate = estimateTokens(result);
2178
2258
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2179
2259
  }
2180
2260
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-crank",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Deterministic vault mutations for Obsidian via MCP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,10 +41,10 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@modelcontextprotocol/sdk": "^1.25.1",
44
- "@velvetmonkey/vault-core": "^0.1.0",
44
+ "@velvetmonkey/vault-core": "^0.2.0",
45
45
  "gray-matter": "^4.0.3",
46
- "zod": "^3.22.4",
47
- "simple-git": "^3.22.0"
46
+ "simple-git": "^3.22.0",
47
+ "zod": "^3.22.4"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^20.10.0",