@velvetmonkey/flywheel-memory 2.0.76 → 2.0.77

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 (3) hide show
  1. package/README.md +26 -30
  2. package/dist/index.js +79 -41
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -12,13 +12,13 @@
12
12
  [![Scale](https://img.shields.io/badge/scale-100k--line%20files%20%7C%202.5k%20entities-brightgreen.svg)](https://github.com/velvetmonkey/flywheel-memory/blob/main/docs/TESTING.md#performance-benchmarks)
13
13
  [![Tests](https://img.shields.io/badge/tests-2,456%20passed-brightgreen.svg)](https://github.com/velvetmonkey/flywheel-memory/blob/main/docs/TESTING.md)
14
14
 
15
- | | Without Flywheel | With Flywheel |
15
+ | | Grep approach | Flywheel |
16
16
  |---|---|---|
17
- | "What's overdue?" | Read every file | Indexed query, <10ms |
18
- | "What links here?" | grep every file | Pre-indexed backlink graph |
17
+ | "What's overdue?" | Grep + read matches (~500-2,000 tokens) | Indexed metadata query (~50-200 tokens) |
18
+ | "What links here?" | Grep for note name (flat list, no graph) | Pre-indexed backlink graph (<10ms) |
19
19
  | "Add a meeting note" | Raw write, no linking | Structured write + auto-wikilink |
20
- | "What should I link?" | Manual or grep | Smart scoring + semantic understanding |
21
- | Token cost | 2,000-250,000 | 50-200 |
20
+ | "What should I link?" | Not possible | 10-dimension scoring + semantic search |
21
+ | Hubs, orphans, paths? | Not possible | Pre-indexed graph analysis |
22
22
 
23
23
  51 tools across 17 categories. 6-line config. Zero cloud dependencies.
24
24
 
@@ -35,8 +35,6 @@ Then ask: *"How much have I billed Acme Corp?"*
35
35
 
36
36
  ## See It Work
37
37
 
38
- ![Flywheel demo](https://raw.githubusercontent.com/velvetmonkey/flywheel-memory/main/demos/flywheel-demo.gif)
39
-
40
38
  ### Read: "How much have I billed Acme Corp?"
41
39
 
42
40
  From the [carter-strategy](https://github.com/velvetmonkey/flywheel-memory/tree/main/demos/carter-strategy/) demo -- a solo consultant with 3 clients, 5 projects, and $27K in invoices.
@@ -44,49 +42,47 @@ From the [carter-strategy](https://github.com/velvetmonkey/flywheel-memory/tree/
44
42
  ```
45
43
  ❯ How much have I billed Acme Corp?
46
44
 
47
- ● flywheel › get_backlinks
48
- path: "clients/Acme Corp.md"
49
- invoices/INV-2025-047.md
50
- invoices/INV-2025-048.md
51
- projects/Acme Data Migration.md
52
- proposals/Acme Analytics Add-on.md
45
+ ● flywheel › search
46
+ query: "Acme Corp"
47
+ clients/Acme Corp.md, invoices/INV-2025-047.md,
48
+ invoices/INV-2025-048.md, projects/Acme Data Migration.md
49
+
50
+ ● read_file("clients/Acme Corp.md")
51
+ ← total_billed: 156000, rate: $300/hr, status: active
53
52
 
54
- flywheel › get_note_metadata
55
- path: "invoices/INV-2025-047.md"
53
+ read_file("invoices/INV-2025-047.md")
56
54
  ← amount: 15000, status: paid, period: November 2025
57
55
 
58
- flywheel › get_note_metadata
59
- path: "invoices/INV-2025-048.md"
56
+ read_file("invoices/INV-2025-048.md")
60
57
  ← amount: 12000, status: pending, period: December 2025
61
58
 
62
59
  ┌─ RESULT ──────────────────────────────────────────────┐
63
- │ Acme Corp Billing: $27,000
60
+ │ Acme Corp: $156K total billed
64
61
  │ │
65
62
  │ Paid: $15,000 — Acme Data Migration (Nov 2025) │
66
63
  │ Pending: $12,000 — Acme Data Migration (Dec 2025) │
67
64
  │ │
68
- 4 tool calls · ~160 tokens · 0 files read
65
+ Also: $35K pending proposal (Analytics Add-on)
69
66
  └───────────────────────────────────────────────────────┘
70
67
  ```
71
68
 
72
- Claude didn't read any files. It navigated the graph: backlinks to find related notes, metadata to extract the numbers.
69
+ Flywheel's indexed search found all Acme-related notes in one call. Claude read the files it needed for billing details. No grepping, no guessing paths.
73
70
 
74
- ```
75
- Same 3 queries without Flywheel: 11,150 tokens (reading files repeatedly)
76
- Same 3 queries with Flywheel: 300 tokens (querying the index)
77
- 37x savings
78
- ```
71
+ Flywheel's search found all related notes in one call. Without it, Claude would grep for "Acme" and scan every matching file.
72
+
73
+ The bigger difference isn't just tokens — it's that Flywheel answers structural questions (backlinks, hubs, shortest paths, schema analysis) that file-level access can't answer at all.
79
74
 
80
75
  ### Write: Auto-wikilinks on every mutation
81
76
 
82
77
  ```
83
- ❯ Log that I finished the Acme strategy deck
78
+ ❯ Log that Stacy Thompson reviewed the API Security Checklist for Acme before the Beta Corp Dashboard kickoff
84
79
 
85
80
  ● flywheel › vault_add_to_section
86
81
  path: "daily-notes/2026-01-04.md"
87
82
  section: "Log"
88
- content: "finished the [[Acme Corp]] strategy deck"
89
- ↑ auto-linked because Acme Corp.md exists
83
+ content: "[[Stacy Thompson]] reviewed the [[API Security Checklist]] for [[Acme Corp|Acme]] before the [[Beta Corp Dashboard]] kickoff → [[GlobalBank API Audit]], [[Acme Analytics Add-on]], [[Acme Data Migration]]"
84
+ 4 entities auto-linked "Acme" resolved to Acme Corp via alias
85
+ → 3 contextual suggestions appended (scored ≥12 via co-occurrence with linked entities)
90
86
  ```
91
87
 
92
88
  Try it yourself: `cd demos/carter-strategy && claude`
@@ -194,8 +190,8 @@ graph LR
194
190
  ```
195
191
 
196
192
  ```
197
- Input: "Met with Sarah about the data migration"
198
- Output: "Met with [[Sarah Mitchell]] about the [[Acme Data Migration]]"
193
+ Input: "Stacy Thompson finished reviewing the API Security Checklist for the Beta Corp Dashboard"
194
+ Output: "[[Stacy Thompson]] finished reviewing the [[API Security Checklist]] for the [[Beta Corp Dashboard]]"
199
195
  ```
200
196
 
201
197
  No manual linking. No broken references. Use compounds into structure, structure compounds into intelligence.
package/dist/index.js CHANGED
@@ -1886,24 +1886,42 @@ async function initEmbeddings() {
1886
1886
  if (pipeline) return;
1887
1887
  if (initPromise) return initPromise;
1888
1888
  initPromise = (async () => {
1889
- try {
1890
- const transformers = await Function("specifier", "return import(specifier)")("@huggingface/transformers");
1891
- pipeline = await transformers.pipeline("feature-extraction", activeModelConfig.id, {
1892
- dtype: "fp32"
1893
- });
1894
- if (activeModelConfig.dims === 0) {
1895
- const probe = await pipeline("test", { pooling: "mean", normalize: true });
1896
- activeModelConfig.dims = probe.data.length;
1897
- console.error(`[Semantic] Probed model ${activeModelConfig.id}: ${activeModelConfig.dims} dims`);
1898
- }
1899
- } catch (err) {
1900
- initPromise = null;
1901
- if (err instanceof Error && (err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Cannot find module") || err.message.includes("ERR_MODULE_NOT_FOUND"))) {
1902
- throw new Error(
1903
- "Semantic search requires @huggingface/transformers. Install it with: npm install @huggingface/transformers"
1904
- );
1889
+ const MAX_RETRIES = 3;
1890
+ const RETRY_DELAYS = [2e3, 5e3, 1e4];
1891
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
1892
+ try {
1893
+ const transformers = await Function("specifier", "return import(specifier)")("@huggingface/transformers");
1894
+ console.error(`[Semantic] Loading model ${activeModelConfig.id} (~23MB, cached after first download)...`);
1895
+ pipeline = await transformers.pipeline("feature-extraction", activeModelConfig.id, {
1896
+ dtype: "fp32"
1897
+ });
1898
+ console.error(`[Semantic] Model loaded successfully`);
1899
+ if (activeModelConfig.dims === 0) {
1900
+ const probe = await pipeline("test", { pooling: "mean", normalize: true });
1901
+ activeModelConfig.dims = probe.data.length;
1902
+ console.error(`[Semantic] Probed model ${activeModelConfig.id}: ${activeModelConfig.dims} dims`);
1903
+ }
1904
+ return;
1905
+ } catch (err) {
1906
+ if (err instanceof Error && (err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Cannot find module") || err.message.includes("ERR_MODULE_NOT_FOUND"))) {
1907
+ initPromise = null;
1908
+ throw new Error(
1909
+ "Semantic search requires @huggingface/transformers. Install it with: npm install @huggingface/transformers"
1910
+ );
1911
+ }
1912
+ if (attempt < MAX_RETRIES) {
1913
+ const delay = RETRY_DELAYS[attempt - 1];
1914
+ console.error(`[Semantic] Model load failed (attempt ${attempt}/${MAX_RETRIES}): ${err instanceof Error ? err.message : err}`);
1915
+ console.error(`[Semantic] Retrying in ${delay / 1e3}s...`);
1916
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
1917
+ pipeline = null;
1918
+ } else {
1919
+ console.error(`[Semantic] Model load failed after ${MAX_RETRIES} attempts: ${err instanceof Error ? err.message : err}`);
1920
+ console.error(`[Semantic] Semantic search disabled. Keyword search (BM25) remains available.`);
1921
+ initPromise = null;
1922
+ throw err;
1923
+ }
1905
1924
  }
1906
- throw err;
1907
1925
  }
1908
1926
  })();
1909
1927
  return initPromise;
@@ -2552,7 +2570,7 @@ function deserializeVaultIndex(data) {
2552
2570
  builtAt: new Date(data.builtAt)
2553
2571
  };
2554
2572
  }
2555
- function loadVaultIndexFromCache(stateDb2, scannedFileCount, maxAgeMs = 24 * 60 * 60 * 1e3, tolerancePercent = 5) {
2573
+ function loadVaultIndexFromCache(stateDb2, scannedFileCount, maxAgeMs = 24 * 60 * 60 * 1e3, tolerancePercent = 5, newestFileMtime) {
2556
2574
  const info = getVaultIndexCacheInfo(stateDb2);
2557
2575
  if (!info) {
2558
2576
  console.error("[Flywheel] No cached index found");
@@ -2569,6 +2587,10 @@ function loadVaultIndexFromCache(stateDb2, scannedFileCount, maxAgeMs = 24 * 60
2569
2587
  console.error(`[Flywheel] Cache invalid: too old (${Math.round(age / 1e3 / 60)} minutes)`);
2570
2588
  return null;
2571
2589
  }
2590
+ if (newestFileMtime && newestFileMtime.getTime() > info.builtAt.getTime()) {
2591
+ console.error(`[Flywheel] Cache invalid: files modified since cache was built (newest file: ${newestFileMtime.toISOString()}, cache built: ${info.builtAt.toISOString()})`);
2592
+ return null;
2593
+ }
2572
2594
  const data = loadVaultIndexCache(stateDb2);
2573
2595
  if (!data) {
2574
2596
  console.error("[Flywheel] Failed to load cached index data");
@@ -19669,7 +19691,8 @@ async function main() {
19669
19691
  const files = await scanVault(vaultPath);
19670
19692
  const noteCount = files.length;
19671
19693
  serverLog("index", `Found ${noteCount} markdown files`);
19672
- cachedIndex = loadVaultIndexFromCache(stateDb, noteCount);
19694
+ const newestMtime = files.reduce((max, f) => f.modified > max ? f.modified : max, /* @__PURE__ */ new Date(0));
19695
+ cachedIndex = loadVaultIndexFromCache(stateDb, noteCount, void 0, void 0, newestMtime);
19673
19696
  } catch (err) {
19674
19697
  serverLog("index", `Cache check failed: ${err instanceof Error ? err.message : err}`, "warn");
19675
19698
  }
@@ -19870,30 +19893,45 @@ async function runPostIndexWork(index) {
19870
19893
  if (hasEmbeddingsIndex() && !modelChanged) {
19871
19894
  serverLog("semantic", "Embeddings already built, skipping full scan");
19872
19895
  } else {
19873
- setEmbeddingsBuilding(true);
19874
- buildEmbeddingsIndex(vaultPath, (p) => {
19875
- if (p.current % 100 === 0 || p.current === p.total) {
19876
- serverLog("semantic", `Embedding ${p.current}/${p.total} notes...`);
19877
- }
19878
- }).then(async () => {
19879
- if (stateDb) {
19880
- const entities = getAllEntitiesFromDb3(stateDb);
19881
- if (entities.length > 0) {
19882
- const entityMap = new Map(entities.map((e) => [e.name, {
19883
- name: e.name,
19884
- path: e.path,
19885
- category: e.category,
19886
- aliases: e.aliases
19887
- }]));
19888
- await buildEntityEmbeddingsIndex(vaultPath, entityMap);
19896
+ const MAX_BUILD_RETRIES = 2;
19897
+ const attemptBuild = async (attempt) => {
19898
+ setEmbeddingsBuilding(true);
19899
+ try {
19900
+ await buildEmbeddingsIndex(vaultPath, (p) => {
19901
+ if (p.current % 100 === 0 || p.current === p.total) {
19902
+ serverLog("semantic", `Embedding ${p.current}/${p.total} notes...`);
19903
+ }
19904
+ });
19905
+ if (stateDb) {
19906
+ const entities = getAllEntitiesFromDb3(stateDb);
19907
+ if (entities.length > 0) {
19908
+ const entityMap = new Map(entities.map((e) => [e.name, {
19909
+ name: e.name,
19910
+ path: e.path,
19911
+ category: e.category,
19912
+ aliases: e.aliases
19913
+ }]));
19914
+ await buildEntityEmbeddingsIndex(vaultPath, entityMap);
19915
+ }
19916
+ }
19917
+ loadEntityEmbeddingsToMemory();
19918
+ setEmbeddingsBuildState("complete");
19919
+ serverLog("semantic", "Embeddings ready \u2014 searches now use hybrid ranking");
19920
+ } catch (err) {
19921
+ const msg = err instanceof Error ? err.message : String(err);
19922
+ if (attempt < MAX_BUILD_RETRIES) {
19923
+ const delay = 1e4;
19924
+ serverLog("semantic", `Build failed (attempt ${attempt}/${MAX_BUILD_RETRIES}): ${msg}. Retrying in ${delay / 1e3}s...`, "error");
19925
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
19926
+ return attemptBuild(attempt + 1);
19889
19927
  }
19928
+ serverLog("semantic", `Embeddings build failed after ${MAX_BUILD_RETRIES} attempts: ${msg}`, "error");
19929
+ serverLog("semantic", "Keyword search (BM25) remains fully available", "error");
19930
+ } finally {
19931
+ setEmbeddingsBuilding(false);
19890
19932
  }
19891
- loadEntityEmbeddingsToMemory();
19892
- setEmbeddingsBuildState("complete");
19893
- serverLog("semantic", "Embeddings ready");
19894
- }).catch((err) => {
19895
- serverLog("semantic", `Embeddings build failed: ${err instanceof Error ? err.message : err}`, "error");
19896
- });
19933
+ };
19934
+ attemptBuild(1);
19897
19935
  }
19898
19936
  } else {
19899
19937
  serverLog("semantic", "Skipping \u2014 FLYWHEEL_SKIP_EMBEDDINGS");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.76",
3
+ "version": "2.0.77",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 51 tools for search, backlinks, graph queries, mutations, agent memory, and hybrid semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@modelcontextprotocol/sdk": "^1.25.1",
55
- "@velvetmonkey/vault-core": "^2.0.75",
55
+ "@velvetmonkey/vault-core": "^2.0.77",
56
56
  "better-sqlite3": "^11.0.0",
57
57
  "chokidar": "^4.0.0",
58
58
  "gray-matter": "^4.0.3",