@velvetmonkey/flywheel-memory 2.0.121 → 2.0.122
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/dist/index.js +268 -11
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -7571,19 +7571,68 @@ function createVaultWatcher(options) {
|
|
|
7571
7571
|
}
|
|
7572
7572
|
|
|
7573
7573
|
// src/core/shared/hubExport.ts
|
|
7574
|
+
var EIGEN_ITERATIONS = 50;
|
|
7574
7575
|
function computeHubScores(index) {
|
|
7575
|
-
const
|
|
7576
|
+
const nodes = [];
|
|
7577
|
+
const nodeIdx = /* @__PURE__ */ new Map();
|
|
7578
|
+
const adj = [];
|
|
7576
7579
|
for (const note of index.notes.values()) {
|
|
7577
|
-
const
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7580
|
+
const key = normalizeTarget(note.path);
|
|
7581
|
+
if (!nodeIdx.has(key)) {
|
|
7582
|
+
nodeIdx.set(key, nodes.length);
|
|
7583
|
+
nodes.push(key);
|
|
7584
|
+
adj.push([]);
|
|
7585
|
+
}
|
|
7586
|
+
const titleKey = note.title.toLowerCase();
|
|
7587
|
+
if (!nodeIdx.has(titleKey)) {
|
|
7588
|
+
nodeIdx.set(titleKey, nodeIdx.get(key));
|
|
7584
7589
|
}
|
|
7585
7590
|
}
|
|
7586
|
-
|
|
7591
|
+
const N = nodes.length;
|
|
7592
|
+
if (N === 0) return /* @__PURE__ */ new Map();
|
|
7593
|
+
for (const note of index.notes.values()) {
|
|
7594
|
+
const fromIdx = nodeIdx.get(normalizeTarget(note.path));
|
|
7595
|
+
if (fromIdx === void 0) continue;
|
|
7596
|
+
for (const link of note.outlinks) {
|
|
7597
|
+
const target = normalizeTarget(link.target);
|
|
7598
|
+
let toIdx = nodeIdx.get(target);
|
|
7599
|
+
if (toIdx === void 0) {
|
|
7600
|
+
toIdx = nodeIdx.get(link.target.toLowerCase());
|
|
7601
|
+
}
|
|
7602
|
+
if (toIdx !== void 0 && toIdx !== fromIdx) {
|
|
7603
|
+
adj[fromIdx].push(toIdx);
|
|
7604
|
+
adj[toIdx].push(fromIdx);
|
|
7605
|
+
}
|
|
7606
|
+
}
|
|
7607
|
+
}
|
|
7608
|
+
let scores = new Float64Array(N).fill(1 / N);
|
|
7609
|
+
for (let iter = 0; iter < EIGEN_ITERATIONS; iter++) {
|
|
7610
|
+
const next = new Float64Array(N);
|
|
7611
|
+
for (let i = 0; i < N; i++) {
|
|
7612
|
+
for (const j of adj[i]) {
|
|
7613
|
+
next[i] += scores[j];
|
|
7614
|
+
}
|
|
7615
|
+
}
|
|
7616
|
+
let norm = 0;
|
|
7617
|
+
for (let i = 0; i < N; i++) norm += next[i] * next[i];
|
|
7618
|
+
norm = Math.sqrt(norm);
|
|
7619
|
+
if (norm > 0) {
|
|
7620
|
+
for (let i = 0; i < N; i++) next[i] /= norm;
|
|
7621
|
+
}
|
|
7622
|
+
scores = next;
|
|
7623
|
+
}
|
|
7624
|
+
let maxScore = 0;
|
|
7625
|
+
for (let i = 0; i < N; i++) {
|
|
7626
|
+
if (scores[i] > maxScore) maxScore = scores[i];
|
|
7627
|
+
}
|
|
7628
|
+
const result = /* @__PURE__ */ new Map();
|
|
7629
|
+
for (let i = 0; i < N; i++) {
|
|
7630
|
+
const scaled = maxScore > 0 ? Math.round(scores[i] / maxScore * 100) : 0;
|
|
7631
|
+
if (scaled > 0) {
|
|
7632
|
+
result.set(nodes[i], scaled);
|
|
7633
|
+
}
|
|
7634
|
+
}
|
|
7635
|
+
return result;
|
|
7587
7636
|
}
|
|
7588
7637
|
function updateHubScoresInDb(stateDb2, hubScores) {
|
|
7589
7638
|
const updateStmt = stateDb2.db.prepare(`
|
|
@@ -8510,6 +8559,162 @@ function getConnectionStrength(index, noteAPath, noteBPath) {
|
|
|
8510
8559
|
}
|
|
8511
8560
|
return { score, factors };
|
|
8512
8561
|
}
|
|
8562
|
+
function computeCentralityMetrics(index, limit = 20) {
|
|
8563
|
+
const paths = Array.from(index.notes.keys());
|
|
8564
|
+
const N = paths.length;
|
|
8565
|
+
if (N === 0) return [];
|
|
8566
|
+
const pathIdx = /* @__PURE__ */ new Map();
|
|
8567
|
+
paths.forEach((p, i) => pathIdx.set(p, i));
|
|
8568
|
+
for (const note of index.notes.values()) {
|
|
8569
|
+
const normalized = note.title.toLowerCase();
|
|
8570
|
+
if (!pathIdx.has(normalized)) {
|
|
8571
|
+
pathIdx.set(normalized, pathIdx.get(note.path));
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
const outAdj = Array.from({ length: N }, () => []);
|
|
8575
|
+
const inAdj = Array.from({ length: N }, () => []);
|
|
8576
|
+
for (const note of index.notes.values()) {
|
|
8577
|
+
const fromIdx = pathIdx.get(note.path);
|
|
8578
|
+
if (fromIdx === void 0) continue;
|
|
8579
|
+
for (const link of note.outlinks) {
|
|
8580
|
+
let toIdx = pathIdx.get(link.target);
|
|
8581
|
+
if (toIdx === void 0) toIdx = pathIdx.get(link.target.toLowerCase());
|
|
8582
|
+
if (toIdx !== void 0 && toIdx !== fromIdx) {
|
|
8583
|
+
outAdj[fromIdx].push(toIdx);
|
|
8584
|
+
inAdj[toIdx].push(fromIdx);
|
|
8585
|
+
}
|
|
8586
|
+
}
|
|
8587
|
+
}
|
|
8588
|
+
const degree = new Float64Array(N);
|
|
8589
|
+
for (let i = 0; i < N; i++) {
|
|
8590
|
+
degree[i] = outAdj[i].length + inAdj[i].length;
|
|
8591
|
+
}
|
|
8592
|
+
const betweenness = new Float64Array(N);
|
|
8593
|
+
const closenessSum = new Float64Array(N);
|
|
8594
|
+
const reachable = new Int32Array(N);
|
|
8595
|
+
const undirAdj = Array.from({ length: N }, () => []);
|
|
8596
|
+
for (let i = 0; i < N; i++) {
|
|
8597
|
+
for (const j of outAdj[i]) {
|
|
8598
|
+
undirAdj[i].push(j);
|
|
8599
|
+
undirAdj[j].push(i);
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8602
|
+
for (let s = 0; s < N; s++) {
|
|
8603
|
+
const stack = [];
|
|
8604
|
+
const pred = Array.from({ length: N }, () => []);
|
|
8605
|
+
const sigma = new Float64Array(N);
|
|
8606
|
+
const dist = new Int32Array(N).fill(-1);
|
|
8607
|
+
sigma[s] = 1;
|
|
8608
|
+
dist[s] = 0;
|
|
8609
|
+
const queue = [s];
|
|
8610
|
+
let head = 0;
|
|
8611
|
+
while (head < queue.length) {
|
|
8612
|
+
const v = queue[head++];
|
|
8613
|
+
stack.push(v);
|
|
8614
|
+
for (const w of undirAdj[v]) {
|
|
8615
|
+
if (dist[w] < 0) {
|
|
8616
|
+
dist[w] = dist[v] + 1;
|
|
8617
|
+
queue.push(w);
|
|
8618
|
+
}
|
|
8619
|
+
if (dist[w] === dist[v] + 1) {
|
|
8620
|
+
sigma[w] += sigma[v];
|
|
8621
|
+
pred[w].push(v);
|
|
8622
|
+
}
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
for (let t = 0; t < N; t++) {
|
|
8626
|
+
if (dist[t] > 0) {
|
|
8627
|
+
closenessSum[s] += dist[t];
|
|
8628
|
+
reachable[s]++;
|
|
8629
|
+
}
|
|
8630
|
+
}
|
|
8631
|
+
const delta = new Float64Array(N);
|
|
8632
|
+
while (stack.length > 0) {
|
|
8633
|
+
const w = stack.pop();
|
|
8634
|
+
for (const v of pred[w]) {
|
|
8635
|
+
delta[v] += sigma[v] / sigma[w] * (1 + delta[w]);
|
|
8636
|
+
}
|
|
8637
|
+
if (w !== s) {
|
|
8638
|
+
betweenness[w] += delta[w];
|
|
8639
|
+
}
|
|
8640
|
+
}
|
|
8641
|
+
}
|
|
8642
|
+
const normFactor = N > 2 ? (N - 1) * (N - 2) : 1;
|
|
8643
|
+
const results = [];
|
|
8644
|
+
for (let i = 0; i < N; i++) {
|
|
8645
|
+
const note = index.notes.get(paths[i]);
|
|
8646
|
+
results.push({
|
|
8647
|
+
path: paths[i],
|
|
8648
|
+
title: note?.title || paths[i],
|
|
8649
|
+
degree: degree[i],
|
|
8650
|
+
betweenness: Math.round(betweenness[i] / normFactor * 1e4) / 1e4,
|
|
8651
|
+
closeness: reachable[i] > 0 ? Math.round(reachable[i] / closenessSum[i] * 1e4) / 1e4 : 0
|
|
8652
|
+
});
|
|
8653
|
+
}
|
|
8654
|
+
results.sort((a, b) => b.betweenness - a.betweenness);
|
|
8655
|
+
return results.slice(0, limit);
|
|
8656
|
+
}
|
|
8657
|
+
function detectCycles(index, maxLength = 10, limit = 20) {
|
|
8658
|
+
const paths = Array.from(index.notes.keys());
|
|
8659
|
+
const N = paths.length;
|
|
8660
|
+
if (N === 0) return [];
|
|
8661
|
+
const pathIdx = /* @__PURE__ */ new Map();
|
|
8662
|
+
paths.forEach((p, i) => pathIdx.set(p, i));
|
|
8663
|
+
for (const note of index.notes.values()) {
|
|
8664
|
+
const normalized = note.title.toLowerCase();
|
|
8665
|
+
if (!pathIdx.has(normalized)) {
|
|
8666
|
+
pathIdx.set(normalized, pathIdx.get(note.path));
|
|
8667
|
+
}
|
|
8668
|
+
}
|
|
8669
|
+
const adj = Array.from({ length: N }, () => []);
|
|
8670
|
+
for (const note of index.notes.values()) {
|
|
8671
|
+
const fromIdx = pathIdx.get(note.path);
|
|
8672
|
+
if (fromIdx === void 0) continue;
|
|
8673
|
+
for (const link of note.outlinks) {
|
|
8674
|
+
let toIdx = pathIdx.get(link.target);
|
|
8675
|
+
if (toIdx === void 0) toIdx = pathIdx.get(link.target.toLowerCase());
|
|
8676
|
+
if (toIdx !== void 0 && toIdx !== fromIdx) {
|
|
8677
|
+
adj[fromIdx].push(toIdx);
|
|
8678
|
+
}
|
|
8679
|
+
}
|
|
8680
|
+
}
|
|
8681
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
8682
|
+
const color = new Uint8Array(N);
|
|
8683
|
+
const cycles = [];
|
|
8684
|
+
const stack = [];
|
|
8685
|
+
function dfs(u) {
|
|
8686
|
+
if (cycles.length >= limit) return;
|
|
8687
|
+
color[u] = GRAY;
|
|
8688
|
+
stack.push(u);
|
|
8689
|
+
for (const v of adj[u]) {
|
|
8690
|
+
if (cycles.length >= limit) return;
|
|
8691
|
+
if (color[v] === GRAY) {
|
|
8692
|
+
const cycleStart = stack.indexOf(v);
|
|
8693
|
+
if (cycleStart >= 0) {
|
|
8694
|
+
const cyclePath = stack.slice(cycleStart);
|
|
8695
|
+
if (cyclePath.length <= maxLength) {
|
|
8696
|
+
const note = (idx) => index.notes.get(paths[idx])?.title || paths[idx];
|
|
8697
|
+
cycles.push({
|
|
8698
|
+
cycle: cyclePath.map(note),
|
|
8699
|
+
length: cyclePath.length
|
|
8700
|
+
});
|
|
8701
|
+
}
|
|
8702
|
+
}
|
|
8703
|
+
} else if (color[v] === WHITE) {
|
|
8704
|
+
dfs(v);
|
|
8705
|
+
}
|
|
8706
|
+
}
|
|
8707
|
+
stack.pop();
|
|
8708
|
+
color[u] = BLACK;
|
|
8709
|
+
}
|
|
8710
|
+
for (let i = 0; i < N; i++) {
|
|
8711
|
+
if (color[i] === WHITE && cycles.length < limit) {
|
|
8712
|
+
dfs(i);
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
cycles.sort((a, b) => a.length - b.length);
|
|
8716
|
+
return cycles;
|
|
8717
|
+
}
|
|
8513
8718
|
|
|
8514
8719
|
// src/core/read/indexGuard.ts
|
|
8515
8720
|
function requireIndex() {
|
|
@@ -9802,6 +10007,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
9802
10007
|
});
|
|
9803
10008
|
const HealthCheckOutputSchema = {
|
|
9804
10009
|
status: z3.enum(["healthy", "degraded", "unhealthy"]).describe("Overall health status"),
|
|
10010
|
+
vault_health_score: z3.coerce.number().describe("Composite vault health score (0-100)"),
|
|
9805
10011
|
schema_version: z3.coerce.number().describe("StateDb schema version"),
|
|
9806
10012
|
vault_accessible: z3.boolean().describe("Whether the vault path is accessible"),
|
|
9807
10013
|
vault_path: z3.string().describe("The vault path being used"),
|
|
@@ -10033,8 +10239,37 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
10033
10239
|
}
|
|
10034
10240
|
}
|
|
10035
10241
|
const topDeadLinkTargets = Array.from(deadTargetCounts.entries()).map(([target, mention_count]) => ({ target, mention_count })).sort((a, b) => b.mention_count - a.mention_count).slice(0, 5);
|
|
10242
|
+
let vault_health_score = 0;
|
|
10243
|
+
if (indexBuilt && noteCount > 0) {
|
|
10244
|
+
const avgOutlinks = linkCount / noteCount;
|
|
10245
|
+
const linkDensity = Math.min(1, avgOutlinks / 3);
|
|
10246
|
+
let orphanCount = 0;
|
|
10247
|
+
for (const note of index.notes.values()) {
|
|
10248
|
+
const bl = index.backlinks.get(note.title.toLowerCase());
|
|
10249
|
+
if (!bl || bl.length === 0) orphanCount++;
|
|
10250
|
+
}
|
|
10251
|
+
const orphanRatio = 1 - orphanCount / noteCount;
|
|
10252
|
+
const totalLinks = linkCount > 0 ? linkCount : 1;
|
|
10253
|
+
const deadLinkRatio = 1 - deadLinkCount / totalLinks;
|
|
10254
|
+
let notesWithFm = 0;
|
|
10255
|
+
for (const note of index.notes.values()) {
|
|
10256
|
+
if (Object.keys(note.frontmatter).length > 0) notesWithFm++;
|
|
10257
|
+
}
|
|
10258
|
+
const fmCoverage = notesWithFm / noteCount;
|
|
10259
|
+
const freshCutoff = Date.now() - 90 * 24 * 60 * 60 * 1e3;
|
|
10260
|
+
let freshCount = 0;
|
|
10261
|
+
for (const note of index.notes.values()) {
|
|
10262
|
+
if (note.modified && note.modified.getTime() > freshCutoff) freshCount++;
|
|
10263
|
+
}
|
|
10264
|
+
const freshness = freshCount / noteCount;
|
|
10265
|
+
const entityCoverage = Math.min(1, entityCount / (noteCount * 0.5));
|
|
10266
|
+
vault_health_score = Math.round(
|
|
10267
|
+
linkDensity * 25 + orphanRatio * 20 + deadLinkRatio * 15 + fmCoverage * 15 + freshness * 15 + entityCoverage * 10
|
|
10268
|
+
);
|
|
10269
|
+
}
|
|
10036
10270
|
const output = {
|
|
10037
10271
|
status,
|
|
10272
|
+
vault_health_score,
|
|
10038
10273
|
schema_version: SCHEMA_VERSION,
|
|
10039
10274
|
vault_accessible: vaultAccessible,
|
|
10040
10275
|
vault_path: vaultPath2,
|
|
@@ -12899,9 +13134,9 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb2
|
|
|
12899
13134
|
"graph_analysis",
|
|
12900
13135
|
{
|
|
12901
13136
|
title: "Graph Analysis",
|
|
12902
|
-
description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (params: folder, limit, offset)\n- "dead_ends": Notes with backlinks but no outgoing links (params: folder, min_backlinks, limit, offset)\n- "sources": Notes with outgoing links but no backlinks (params: folder, min_outlinks, limit, offset)\n- "hubs": Highly connected notes (params: min_links, limit, offset)\n- "stale": Important notes not recently modified (params: days [required], min_backlinks, limit, offset)\n- "immature": Notes scored by maturity (params: folder, limit, offset)\n- "emerging_hubs": Entities growing fastest in connection count (params: days, limit, offset)',
|
|
13137
|
+
description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (params: folder, limit, offset)\n- "dead_ends": Notes with backlinks but no outgoing links (params: folder, min_backlinks, limit, offset)\n- "sources": Notes with outgoing links but no backlinks (params: folder, min_outlinks, limit, offset)\n- "hubs": Highly connected notes (params: min_links, limit, offset)\n- "stale": Important notes not recently modified (params: days [required], min_backlinks, limit, offset)\n- "immature": Notes scored by maturity (params: folder, limit, offset)\n- "emerging_hubs": Entities growing fastest in connection count (params: days, limit, offset)\n- "centrality": Degree, betweenness, and closeness centrality metrics (params: limit, offset)\n- "cycles": Detect circular link chains in the vault (params: limit)',
|
|
12903
13138
|
inputSchema: {
|
|
12904
|
-
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale", "immature", "emerging_hubs"]).describe("Type of graph analysis to perform"),
|
|
13139
|
+
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale", "immature", "emerging_hubs", "centrality", "cycles"]).describe("Type of graph analysis to perform"),
|
|
12905
13140
|
folder: z8.string().optional().describe("Limit to notes in this folder (orphans, dead_ends, sources, immature)"),
|
|
12906
13141
|
min_links: z8.coerce.number().default(5).describe("Minimum total connections for hubs"),
|
|
12907
13142
|
min_backlinks: z8.coerce.number().default(1).describe("Minimum backlinks (dead_ends, stale)"),
|
|
@@ -13100,6 +13335,28 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb2
|
|
|
13100
13335
|
}, null, 2) }]
|
|
13101
13336
|
};
|
|
13102
13337
|
}
|
|
13338
|
+
case "centrality": {
|
|
13339
|
+
const results = computeCentralityMetrics(index, limit);
|
|
13340
|
+
const paginated = results.slice(offset, offset + limit);
|
|
13341
|
+
return {
|
|
13342
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
13343
|
+
analysis: "centrality",
|
|
13344
|
+
total_count: results.length,
|
|
13345
|
+
returned_count: paginated.length,
|
|
13346
|
+
notes: paginated
|
|
13347
|
+
}, null, 2) }]
|
|
13348
|
+
};
|
|
13349
|
+
}
|
|
13350
|
+
case "cycles": {
|
|
13351
|
+
const cycles = detectCycles(index, 10, limit);
|
|
13352
|
+
return {
|
|
13353
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
13354
|
+
analysis: "cycles",
|
|
13355
|
+
total_count: cycles.length,
|
|
13356
|
+
cycles
|
|
13357
|
+
}, null, 2) }]
|
|
13358
|
+
};
|
|
13359
|
+
}
|
|
13103
13360
|
}
|
|
13104
13361
|
}
|
|
13105
13362
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.122",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 69 tools for search, backlinks, graph queries, mutations, agent memory, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"lint": "tsc --noEmit",
|
|
50
50
|
"clean": "rm -rf dist",
|
|
51
51
|
"postinstall": "node -e \"['bin/flywheel-memory.js','dist/index.js'].forEach(f=>{try{require('fs').chmodSync(f,0o755)}catch{}})\"",
|
|
52
|
-
"prepublishOnly": "npm run build"
|
|
52
|
+
"prepublishOnly": "npm run lint && npm run build"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
56
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
56
|
+
"@velvetmonkey/vault-core": "^2.0.122",
|
|
57
57
|
"better-sqlite3": "^11.0.0",
|
|
58
58
|
"chokidar": "^4.0.0",
|
|
59
59
|
"gray-matter": "^4.0.3",
|