@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.
- package/README.md +26 -30
- package/dist/index.js +79 -41
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
[](https://github.com/velvetmonkey/flywheel-memory/blob/main/docs/TESTING.md#performance-benchmarks)
|
|
13
13
|
[](https://github.com/velvetmonkey/flywheel-memory/blob/main/docs/TESTING.md)
|
|
14
14
|
|
|
15
|
-
| |
|
|
15
|
+
| | Grep approach | Flywheel |
|
|
16
16
|
|---|---|---|
|
|
17
|
-
| "What's overdue?" |
|
|
18
|
-
| "What links here?" |
|
|
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?" |
|
|
21
|
-
|
|
|
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
|
-

|
|
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 ›
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
invoices/INV-2025-048.md
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
●
|
|
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
|
-
●
|
|
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
|
|
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
|
-
│
|
|
65
|
+
│ Also: $35K pending proposal (Analytics Add-on) │
|
|
69
66
|
└───────────────────────────────────────────────────────┘
|
|
70
67
|
```
|
|
71
68
|
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
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: "
|
|
89
|
-
|
|
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: "
|
|
198
|
-
Output: "
|
|
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
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19874
|
-
|
|
19875
|
-
|
|
19876
|
-
|
|
19877
|
-
|
|
19878
|
-
|
|
19879
|
-
|
|
19880
|
-
|
|
19881
|
-
|
|
19882
|
-
|
|
19883
|
-
|
|
19884
|
-
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
|
|
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
|
-
|
|
19892
|
-
|
|
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.
|
|
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.
|
|
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",
|