@velvetmonkey/flywheel-memory 2.0.30 → 2.0.32

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 +79 -8
  2. package/package.json +84 -84
package/dist/index.js CHANGED
@@ -3216,6 +3216,7 @@ import {
3216
3216
  getEntityAliases,
3217
3217
  applyWikilinks,
3218
3218
  resolveAliasWikilinks,
3219
+ detectImplicitEntities,
3219
3220
  getEntityIndexFromDb,
3220
3221
  getStateDbMetadata,
3221
3222
  getEntityByName,
@@ -5060,6 +5061,42 @@ function processWikilinks(content, notePath) {
5060
5061
  firstOccurrenceOnly: true,
5061
5062
  caseInsensitive: true
5062
5063
  });
5064
+ const implicitMatches = detectImplicitEntities(result.content, {
5065
+ detectImplicit: true,
5066
+ implicitPatterns: ["proper-nouns", "single-caps", "quoted-terms", "camel-case", "acronyms"],
5067
+ minEntityLength: 3
5068
+ });
5069
+ const alreadyLinked = new Set(
5070
+ [...resolved.linkedEntities, ...result.linkedEntities].map((e) => e.toLowerCase())
5071
+ );
5072
+ for (const entity of sortedEntities) {
5073
+ const name = getEntityName2(entity);
5074
+ alreadyLinked.add(name.toLowerCase());
5075
+ const aliases = getEntityAliases(entity);
5076
+ for (const alias of aliases) {
5077
+ alreadyLinked.add(alias.toLowerCase());
5078
+ }
5079
+ }
5080
+ const currentNoteName = notePath ? notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() : null;
5081
+ const newImplicits = implicitMatches.filter((m) => {
5082
+ const normalized = m.text.toLowerCase();
5083
+ if (alreadyLinked.has(normalized)) return false;
5084
+ if (currentNoteName && normalized === currentNoteName) return false;
5085
+ return true;
5086
+ });
5087
+ if (newImplicits.length > 0) {
5088
+ let processedContent = result.content;
5089
+ for (let i = newImplicits.length - 1; i >= 0; i--) {
5090
+ const m = newImplicits[i];
5091
+ processedContent = processedContent.slice(0, m.start) + `[[${m.text}]]` + processedContent.slice(m.end);
5092
+ }
5093
+ return {
5094
+ content: processedContent,
5095
+ linksAdded: resolved.linksAdded + result.linksAdded + newImplicits.length,
5096
+ linkedEntities: [...resolved.linkedEntities, ...result.linkedEntities],
5097
+ implicitEntities: newImplicits.map((m) => m.text)
5098
+ };
5099
+ }
5063
5100
  return {
5064
5101
  content: result.content,
5065
5102
  linksAdded: resolved.linksAdded + result.linksAdded,
@@ -5076,9 +5113,11 @@ function maybeApplyWikilinks(content, skipWikilinks, notePath) {
5076
5113
  if (moduleStateDb4 && notePath) {
5077
5114
  trackWikilinkApplications(moduleStateDb4, notePath, result.linkedEntities);
5078
5115
  }
5116
+ const implicitCount = result.implicitEntities?.length ?? 0;
5117
+ const implicitInfo = implicitCount > 0 ? ` + ${implicitCount} implicit: ${result.implicitEntities.join(", ")}` : "";
5079
5118
  return {
5080
5119
  content: result.content,
5081
- wikilinkInfo: `Applied ${result.linksAdded} wikilink(s): ${result.linkedEntities.join(", ")}`
5120
+ wikilinkInfo: `Applied ${result.linksAdded} wikilink(s): ${result.linkedEntities.join(", ")}${implicitInfo}`
5082
5121
  };
5083
5122
  }
5084
5123
  return { content: result.content };
@@ -7361,6 +7400,37 @@ function rowToEvent(row) {
7361
7400
  steps: row.steps ? JSON.parse(row.steps) : null
7362
7401
  };
7363
7402
  }
7403
+ function getRecentPipelineEvent(stateDb2) {
7404
+ const row = stateDb2.db.prepare(
7405
+ "SELECT * FROM index_events WHERE steps IS NOT NULL ORDER BY timestamp DESC LIMIT 1"
7406
+ ).get();
7407
+ return row ? rowToEvent(row) : null;
7408
+ }
7409
+ function computeEntityDiff(before, after) {
7410
+ const beforeMap = new Map(before.map((e) => [e.nameLower, e]));
7411
+ const afterMap = new Map(after.map((e) => [e.nameLower, e]));
7412
+ const added = [];
7413
+ const removed = [];
7414
+ const alias_changes = [];
7415
+ for (const [key, entity] of afterMap) {
7416
+ if (!beforeMap.has(key)) {
7417
+ added.push(entity.name);
7418
+ } else {
7419
+ const prev = beforeMap.get(key);
7420
+ const prevAliases = JSON.stringify(prev.aliases.sort());
7421
+ const currAliases = JSON.stringify(entity.aliases.sort());
7422
+ if (prevAliases !== currAliases) {
7423
+ alias_changes.push({ entity: entity.name, before: prev.aliases, after: entity.aliases });
7424
+ }
7425
+ }
7426
+ }
7427
+ for (const [key, entity] of beforeMap) {
7428
+ if (!afterMap.has(key)) {
7429
+ removed.push(entity.name);
7430
+ }
7431
+ }
7432
+ return { added, removed, alias_changes };
7433
+ }
7364
7434
  function getRecentIndexEvents(stateDb2, limit = 20) {
7365
7435
  const rows = stateDb2.db.prepare(
7366
7436
  "SELECT * FROM index_events ORDER BY timestamp DESC LIMIT ?"
@@ -7558,9 +7628,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7558
7628
  let lastPipeline;
7559
7629
  if (stateDb2) {
7560
7630
  try {
7561
- const events = getRecentIndexEvents(stateDb2, 1);
7562
- if (events.length > 0 && events[0].steps && events[0].steps.length > 0) {
7563
- const evt = events[0];
7631
+ const evt = getRecentPipelineEvent(stateDb2);
7632
+ if (evt && evt.steps && evt.steps.length > 0) {
7564
7633
  lastPipeline = {
7565
7634
  timestamp: evt.timestamp,
7566
7635
  trigger: evt.trigger,
@@ -16101,12 +16170,14 @@ async function runPostIndexWork(index) {
16101
16170
  setIndexState("ready");
16102
16171
  tracker.end({ note_count: vaultIndex.notes.size, entity_count: vaultIndex.entities.size, tag_count: vaultIndex.tags.size });
16103
16172
  serverLog("watcher", `Index rebuilt: ${vaultIndex.notes.size} notes, ${vaultIndex.entities.size} entities`);
16173
+ const entitiesBefore = stateDb ? getAllEntitiesFromDb3(stateDb) : [];
16104
16174
  tracker.start("entity_scan", { note_count: vaultIndex.notes.size });
16105
16175
  await updateEntitiesInStateDb();
16106
- const entityCount = stateDb ? getAllEntitiesFromDb3(stateDb).length : 0;
16107
- tracker.end({ entity_count: entityCount });
16108
- serverLog("watcher", `Entity scan: ${entityCount} entities`);
16109
- tracker.start("hub_scores", { entity_count: entityCount });
16176
+ const entitiesAfter = stateDb ? getAllEntitiesFromDb3(stateDb) : [];
16177
+ const entityDiff = computeEntityDiff(entitiesBefore, entitiesAfter);
16178
+ tracker.end({ entity_count: entitiesAfter.length, ...entityDiff });
16179
+ serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
16180
+ tracker.start("hub_scores", { entity_count: entitiesAfter.length });
16110
16181
  const hubUpdated = await exportHubScores(vaultIndex, stateDb);
16111
16182
  tracker.end({ updated: hubUpdated ?? 0 });
16112
16183
  serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
package/package.json CHANGED
@@ -1,84 +1,84 @@
1
- {
2
- "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.30",
4
- "description": "MCP server that gives Claude full read/write access to your Obsidian vault. 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "bin": {
8
- "flywheel-memory": "dist/index.js"
9
- },
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/velvetmonkey/flywheel-memory.git",
13
- "directory": "packages/mcp-server"
14
- },
15
- "bugs": {
16
- "url": "https://github.com/velvetmonkey/flywheel-memory/issues"
17
- },
18
- "homepage": "https://github.com/velvetmonkey/flywheel-memory#readme",
19
- "author": "velvetmonkey",
20
- "keywords": [
21
- "mcp",
22
- "mcp-server",
23
- "obsidian",
24
- "pkm",
25
- "markdown",
26
- "knowledge-graph",
27
- "wikilinks",
28
- "backlinks",
29
- "vault",
30
- "claude",
31
- "claude-code",
32
- "local-first",
33
- "daily-notes",
34
- "zettelkasten"
35
- ],
36
- "scripts": {
37
- "build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external && chmod +x dist/index.js",
38
- "dev": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external --watch",
39
- "test": "vitest run",
40
- "test:watch": "vitest",
41
- "test:read": "vitest run test/read/",
42
- "test:write": "vitest run test/write/",
43
- "test:security": "vitest run test/write/security/",
44
- "test:stress": "vitest run test/write/stress/ test/write/battle-hardening/",
45
- "test:coverage": "vitest run --coverage",
46
- "test:ci": "vitest run --reporter=github-actions",
47
- "lint": "tsc --noEmit",
48
- "clean": "rm -rf dist",
49
- "prepublishOnly": "npm run build"
50
- },
51
- "dependencies": {
52
- "@modelcontextprotocol/sdk": "^1.25.1",
53
- "@velvetmonkey/vault-core": "^2.0.30",
54
- "better-sqlite3": "^11.0.0",
55
- "chokidar": "^4.0.0",
56
- "gray-matter": "^4.0.3",
57
- "simple-git": "^3.22.0",
58
- "zod": "^3.22.4",
59
- "@huggingface/transformers": "^3.8.1"
60
- },
61
- "devDependencies": {
62
- "@types/better-sqlite3": "^7.6.0",
63
- "@types/node": "^20.10.0",
64
- "@vitest/coverage-v8": "^2.0.0",
65
- "esbuild": "^0.24.0",
66
- "fast-check": "^3.15.0",
67
- "mcp-testing-kit": "^0.2.0",
68
- "tsx": "^4.19.0",
69
- "typescript": "^5.3.2",
70
- "vitest": "^2.0.0"
71
- },
72
- "engines": {
73
- "node": ">=18.0.0"
74
- },
75
- "license": "AGPL-3.0-only",
76
- "files": [
77
- "dist",
78
- "README.md",
79
- "LICENSE"
80
- ],
81
- "publishConfig": {
82
- "access": "public"
83
- }
84
- }
1
+ {
2
+ "name": "@velvetmonkey/flywheel-memory",
3
+ "version": "2.0.32",
4
+ "description": "MCP server that gives Claude full read/write access to your Obsidian vault. 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "flywheel-memory": "dist/index.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/velvetmonkey/flywheel-memory.git",
13
+ "directory": "packages/mcp-server"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/velvetmonkey/flywheel-memory/issues"
17
+ },
18
+ "homepage": "https://github.com/velvetmonkey/flywheel-memory#readme",
19
+ "author": "velvetmonkey",
20
+ "keywords": [
21
+ "mcp",
22
+ "mcp-server",
23
+ "obsidian",
24
+ "pkm",
25
+ "markdown",
26
+ "knowledge-graph",
27
+ "wikilinks",
28
+ "backlinks",
29
+ "vault",
30
+ "claude",
31
+ "claude-code",
32
+ "local-first",
33
+ "daily-notes",
34
+ "zettelkasten"
35
+ ],
36
+ "scripts": {
37
+ "build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external && chmod +x dist/index.js",
38
+ "dev": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external --watch",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "test:read": "vitest run test/read/",
42
+ "test:write": "vitest run test/write/",
43
+ "test:security": "vitest run test/write/security/",
44
+ "test:stress": "vitest run test/write/stress/ test/write/battle-hardening/",
45
+ "test:coverage": "vitest run --coverage",
46
+ "test:ci": "vitest run --reporter=github-actions",
47
+ "lint": "tsc --noEmit",
48
+ "clean": "rm -rf dist",
49
+ "prepublishOnly": "npm run build"
50
+ },
51
+ "dependencies": {
52
+ "@modelcontextprotocol/sdk": "^1.25.1",
53
+ "@velvetmonkey/vault-core": "^2.0.32",
54
+ "better-sqlite3": "^11.0.0",
55
+ "chokidar": "^4.0.0",
56
+ "gray-matter": "^4.0.3",
57
+ "simple-git": "^3.22.0",
58
+ "zod": "^3.22.4",
59
+ "@huggingface/transformers": "^3.8.1"
60
+ },
61
+ "devDependencies": {
62
+ "@types/better-sqlite3": "^7.6.0",
63
+ "@types/node": "^20.10.0",
64
+ "@vitest/coverage-v8": "^2.0.0",
65
+ "esbuild": "^0.24.0",
66
+ "fast-check": "^3.15.0",
67
+ "mcp-testing-kit": "^0.2.0",
68
+ "tsx": "^4.19.0",
69
+ "typescript": "^5.3.2",
70
+ "vitest": "^2.0.0"
71
+ },
72
+ "engines": {
73
+ "node": ">=18.0.0"
74
+ },
75
+ "license": "AGPL-3.0-only",
76
+ "files": [
77
+ "dist",
78
+ "README.md",
79
+ "LICENSE"
80
+ ],
81
+ "publishConfig": {
82
+ "access": "public"
83
+ }
84
+ }