@velvetmonkey/flywheel-memory 2.0.76 → 2.0.78
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 +154 -62
- 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");
|
|
@@ -9570,7 +9592,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
9570
9592
|
import { z as z4 } from "zod";
|
|
9571
9593
|
import {
|
|
9572
9594
|
searchEntities,
|
|
9573
|
-
searchEntitiesPrefix
|
|
9595
|
+
searchEntitiesPrefix,
|
|
9596
|
+
getEntityByName as getEntityByName2
|
|
9574
9597
|
} from "@velvetmonkey/vault-core";
|
|
9575
9598
|
function matchesFrontmatter(note, where) {
|
|
9576
9599
|
for (const [key, value] of Object.entries(where)) {
|
|
@@ -9618,6 +9641,37 @@ function inFolder(note, folder) {
|
|
|
9618
9641
|
const normalizedFolder = folder.endsWith("/") ? folder : folder + "/";
|
|
9619
9642
|
return note.path.startsWith(normalizedFolder) || note.path.split("/")[0] === folder.replace("/", "");
|
|
9620
9643
|
}
|
|
9644
|
+
function enrichResult(result, index, stateDb2) {
|
|
9645
|
+
const note = index.notes.get(result.path);
|
|
9646
|
+
const normalizedPath = result.path.toLowerCase().replace(/\.md$/, "");
|
|
9647
|
+
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
9648
|
+
const enriched = {
|
|
9649
|
+
path: result.path,
|
|
9650
|
+
title: result.title
|
|
9651
|
+
};
|
|
9652
|
+
if (result.snippet) enriched.snippet = result.snippet;
|
|
9653
|
+
if (note) {
|
|
9654
|
+
enriched.frontmatter = note.frontmatter;
|
|
9655
|
+
enriched.tags = note.tags;
|
|
9656
|
+
enriched.aliases = note.aliases;
|
|
9657
|
+
enriched.backlink_count = backlinks.length;
|
|
9658
|
+
enriched.outlink_count = note.outlinks.length;
|
|
9659
|
+
enriched.modified = note.modified.toISOString();
|
|
9660
|
+
if (note.created) enriched.created = note.created.toISOString();
|
|
9661
|
+
}
|
|
9662
|
+
if (stateDb2) {
|
|
9663
|
+
try {
|
|
9664
|
+
const entity = getEntityByName2(stateDb2, result.title);
|
|
9665
|
+
if (entity) {
|
|
9666
|
+
enriched.category = entity.category;
|
|
9667
|
+
enriched.hub_score = entity.hubScore;
|
|
9668
|
+
if (entity.description) enriched.description = entity.description;
|
|
9669
|
+
}
|
|
9670
|
+
} catch {
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9673
|
+
return enriched;
|
|
9674
|
+
}
|
|
9621
9675
|
function sortNotes(notes, sortBy, order) {
|
|
9622
9676
|
const sorted = [...notes];
|
|
9623
9677
|
sorted.sort((a, b) => {
|
|
@@ -9825,22 +9879,20 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9825
9879
|
const fts5Map = new Map(fts5Results.map((r) => [normalizePath2(r.path), r]));
|
|
9826
9880
|
const semanticMap = new Map(semanticResults.map((r) => [normalizePath2(r.path), r]));
|
|
9827
9881
|
const entityMap = new Map(entityResults.map((r) => [normalizePath2(r.path), r]));
|
|
9828
|
-
const merged = Array.from(allPaths).map((p) =>
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
}));
|
|
9837
|
-
merged.sort((a, b) => b.rrf_score - a.rrf_score);
|
|
9882
|
+
const merged = Array.from(allPaths).map((p) => {
|
|
9883
|
+
const title = fts5Map.get(p)?.title || semanticMap.get(p)?.title || entityMap.get(p)?.name || p.replace(/\.md$/, "").split("/").pop() || p;
|
|
9884
|
+
const snippet = fts5Map.get(p)?.snippet;
|
|
9885
|
+
const rrfScore = rrfScores.get(p) || 0;
|
|
9886
|
+
return { rrfScore, ...enrichResult({ path: p, title, snippet }, index, getStateDb()) };
|
|
9887
|
+
});
|
|
9888
|
+
merged.sort((a, b) => b.rrfScore - a.rrfScore);
|
|
9889
|
+
const results = merged.slice(0, limit).map(({ rrfScore, ...rest }) => rest);
|
|
9838
9890
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
9839
9891
|
scope,
|
|
9840
9892
|
method: "hybrid",
|
|
9841
9893
|
query,
|
|
9842
9894
|
total_results: Math.min(merged.length, limit),
|
|
9843
|
-
results
|
|
9895
|
+
results
|
|
9844
9896
|
}, null, 2) }] };
|
|
9845
9897
|
} catch (err) {
|
|
9846
9898
|
console.error("[Semantic] Hybrid search failed, falling back to FTS5:", err instanceof Error ? err.message : err);
|
|
@@ -9849,11 +9901,10 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9849
9901
|
if (entityResults.length > 0) {
|
|
9850
9902
|
const normalizePath2 = (p) => p.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
9851
9903
|
const fts5Map = new Map(fts5Results.map((r) => [normalizePath2(r.path), r]));
|
|
9852
|
-
const entityNormMap = new Map(entityResults.map((r) => [normalizePath2(r.path), r]));
|
|
9853
9904
|
const entityRanked = entityResults.filter((r) => !fts5Map.has(normalizePath2(r.path)));
|
|
9854
9905
|
const merged = [
|
|
9855
|
-
...fts5Results.map((r) => ({ path: r.path, title: r.title, snippet: r.snippet,
|
|
9856
|
-
...entityRanked.map((r) => ({ path: r.path, title: r.name,
|
|
9906
|
+
...fts5Results.map((r) => enrichResult({ path: r.path, title: r.title, snippet: r.snippet }, index, getStateDb())),
|
|
9907
|
+
...entityRanked.map((r) => enrichResult({ path: r.path, title: r.name }, index, getStateDb()))
|
|
9857
9908
|
];
|
|
9858
9909
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
9859
9910
|
scope: "content",
|
|
@@ -9863,12 +9914,13 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9863
9914
|
results: merged.slice(0, limit)
|
|
9864
9915
|
}, null, 2) }] };
|
|
9865
9916
|
}
|
|
9917
|
+
const enrichedFts5 = fts5Results.map((r) => enrichResult({ path: r.path, title: r.title, snippet: r.snippet }, index, getStateDb()));
|
|
9866
9918
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
9867
9919
|
scope: "content",
|
|
9868
9920
|
method: "fts5",
|
|
9869
9921
|
query,
|
|
9870
|
-
total_results:
|
|
9871
|
-
results:
|
|
9922
|
+
total_results: enrichedFts5.length,
|
|
9923
|
+
results: enrichedFts5
|
|
9872
9924
|
}, null, 2) }] };
|
|
9873
9925
|
}
|
|
9874
9926
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Invalid scope" }, null, 2) }] };
|
|
@@ -10679,7 +10731,8 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
10679
10731
|
}
|
|
10680
10732
|
|
|
10681
10733
|
// src/tools/read/primitives.ts
|
|
10682
|
-
|
|
10734
|
+
import { getEntityByName as getEntityByName3 } from "@velvetmonkey/vault-core";
|
|
10735
|
+
function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = () => ({}), getStateDb = () => null) {
|
|
10683
10736
|
server2.registerTool(
|
|
10684
10737
|
"get_note_structure",
|
|
10685
10738
|
{
|
|
@@ -10707,8 +10760,31 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10707
10760
|
}
|
|
10708
10761
|
}
|
|
10709
10762
|
}
|
|
10763
|
+
const note = index.notes.get(path33);
|
|
10764
|
+
const enriched = { ...result };
|
|
10765
|
+
if (note) {
|
|
10766
|
+
enriched.frontmatter = note.frontmatter;
|
|
10767
|
+
enriched.tags = note.tags;
|
|
10768
|
+
enriched.aliases = note.aliases;
|
|
10769
|
+
const normalizedPath = path33.toLowerCase().replace(/\.md$/, "");
|
|
10770
|
+
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
10771
|
+
enriched.backlink_count = backlinks.length;
|
|
10772
|
+
enriched.outlink_count = note.outlinks.length;
|
|
10773
|
+
}
|
|
10774
|
+
const stateDb2 = getStateDb();
|
|
10775
|
+
if (stateDb2 && note) {
|
|
10776
|
+
try {
|
|
10777
|
+
const entity = getEntityByName3(stateDb2, note.title);
|
|
10778
|
+
if (entity) {
|
|
10779
|
+
enriched.category = entity.category;
|
|
10780
|
+
enriched.hub_score = entity.hubScore;
|
|
10781
|
+
if (entity.description) enriched.description = entity.description;
|
|
10782
|
+
}
|
|
10783
|
+
} catch {
|
|
10784
|
+
}
|
|
10785
|
+
}
|
|
10710
10786
|
return {
|
|
10711
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
10787
|
+
content: [{ type: "text", text: JSON.stringify(enriched, null, 2) }]
|
|
10712
10788
|
};
|
|
10713
10789
|
}
|
|
10714
10790
|
);
|
|
@@ -19363,7 +19439,7 @@ var ALL_CATEGORIES = [
|
|
|
19363
19439
|
"policy",
|
|
19364
19440
|
"memory"
|
|
19365
19441
|
];
|
|
19366
|
-
var DEFAULT_PRESET = "
|
|
19442
|
+
var DEFAULT_PRESET = "minimal";
|
|
19367
19443
|
function parseEnabledCategories() {
|
|
19368
19444
|
const envValue = (process.env.FLYWHEEL_TOOLS ?? process.env.FLYWHEEL_PRESET)?.trim();
|
|
19369
19445
|
if (!envValue) {
|
|
@@ -19565,7 +19641,7 @@ registerSystemTools(
|
|
|
19565
19641
|
registerGraphTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
19566
19642
|
registerWikilinkTools(server, () => vaultIndex, () => vaultPath);
|
|
19567
19643
|
registerQueryTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
19568
|
-
registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
|
|
19644
|
+
registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
|
|
19569
19645
|
registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb, () => flywheelConfig);
|
|
19570
19646
|
registerVaultSchemaTools(server, () => vaultIndex, () => vaultPath);
|
|
19571
19647
|
registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
|
|
@@ -19669,7 +19745,8 @@ async function main() {
|
|
|
19669
19745
|
const files = await scanVault(vaultPath);
|
|
19670
19746
|
const noteCount = files.length;
|
|
19671
19747
|
serverLog("index", `Found ${noteCount} markdown files`);
|
|
19672
|
-
|
|
19748
|
+
const newestMtime = files.reduce((max, f) => f.modified > max ? f.modified : max, /* @__PURE__ */ new Date(0));
|
|
19749
|
+
cachedIndex = loadVaultIndexFromCache(stateDb, noteCount, void 0, void 0, newestMtime);
|
|
19673
19750
|
} catch (err) {
|
|
19674
19751
|
serverLog("index", `Cache check failed: ${err instanceof Error ? err.message : err}`, "warn");
|
|
19675
19752
|
}
|
|
@@ -19870,30 +19947,45 @@ async function runPostIndexWork(index) {
|
|
|
19870
19947
|
if (hasEmbeddingsIndex() && !modelChanged) {
|
|
19871
19948
|
serverLog("semantic", "Embeddings already built, skipping full scan");
|
|
19872
19949
|
} else {
|
|
19873
|
-
|
|
19874
|
-
|
|
19875
|
-
|
|
19876
|
-
|
|
19877
|
-
|
|
19878
|
-
|
|
19879
|
-
|
|
19880
|
-
|
|
19881
|
-
|
|
19882
|
-
|
|
19883
|
-
|
|
19884
|
-
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
|
|
19950
|
+
const MAX_BUILD_RETRIES = 2;
|
|
19951
|
+
const attemptBuild = async (attempt) => {
|
|
19952
|
+
setEmbeddingsBuilding(true);
|
|
19953
|
+
try {
|
|
19954
|
+
await buildEmbeddingsIndex(vaultPath, (p) => {
|
|
19955
|
+
if (p.current % 100 === 0 || p.current === p.total) {
|
|
19956
|
+
serverLog("semantic", `Embedding ${p.current}/${p.total} notes...`);
|
|
19957
|
+
}
|
|
19958
|
+
});
|
|
19959
|
+
if (stateDb) {
|
|
19960
|
+
const entities = getAllEntitiesFromDb3(stateDb);
|
|
19961
|
+
if (entities.length > 0) {
|
|
19962
|
+
const entityMap = new Map(entities.map((e) => [e.name, {
|
|
19963
|
+
name: e.name,
|
|
19964
|
+
path: e.path,
|
|
19965
|
+
category: e.category,
|
|
19966
|
+
aliases: e.aliases
|
|
19967
|
+
}]));
|
|
19968
|
+
await buildEntityEmbeddingsIndex(vaultPath, entityMap);
|
|
19969
|
+
}
|
|
19889
19970
|
}
|
|
19971
|
+
loadEntityEmbeddingsToMemory();
|
|
19972
|
+
setEmbeddingsBuildState("complete");
|
|
19973
|
+
serverLog("semantic", "Embeddings ready \u2014 searches now use hybrid ranking");
|
|
19974
|
+
} catch (err) {
|
|
19975
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19976
|
+
if (attempt < MAX_BUILD_RETRIES) {
|
|
19977
|
+
const delay = 1e4;
|
|
19978
|
+
serverLog("semantic", `Build failed (attempt ${attempt}/${MAX_BUILD_RETRIES}): ${msg}. Retrying in ${delay / 1e3}s...`, "error");
|
|
19979
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
19980
|
+
return attemptBuild(attempt + 1);
|
|
19981
|
+
}
|
|
19982
|
+
serverLog("semantic", `Embeddings build failed after ${MAX_BUILD_RETRIES} attempts: ${msg}`, "error");
|
|
19983
|
+
serverLog("semantic", "Keyword search (BM25) remains fully available", "error");
|
|
19984
|
+
} finally {
|
|
19985
|
+
setEmbeddingsBuilding(false);
|
|
19890
19986
|
}
|
|
19891
|
-
|
|
19892
|
-
|
|
19893
|
-
serverLog("semantic", "Embeddings ready");
|
|
19894
|
-
}).catch((err) => {
|
|
19895
|
-
serverLog("semantic", `Embeddings build failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
19896
|
-
});
|
|
19987
|
+
};
|
|
19988
|
+
attemptBuild(1);
|
|
19897
19989
|
}
|
|
19898
19990
|
} else {
|
|
19899
19991
|
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.78",
|
|
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.78",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|