@velvetmonkey/flywheel-memory 2.0.121 → 2.0.123
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 +481 -93
- 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));
|
|
7589
|
+
}
|
|
7590
|
+
}
|
|
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
|
+
}
|
|
7584
7606
|
}
|
|
7585
7607
|
}
|
|
7586
|
-
|
|
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,
|
|
@@ -10485,7 +10720,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
10485
10720
|
import { z as z4 } from "zod";
|
|
10486
10721
|
init_embeddings();
|
|
10487
10722
|
import {
|
|
10488
|
-
searchEntities,
|
|
10723
|
+
searchEntities as searchEntities2,
|
|
10489
10724
|
searchEntitiesPrefix
|
|
10490
10725
|
} from "@velvetmonkey/vault-core";
|
|
10491
10726
|
|
|
@@ -10494,7 +10729,6 @@ import {
|
|
|
10494
10729
|
getEntityByName as getEntityByName2
|
|
10495
10730
|
} from "@velvetmonkey/vault-core";
|
|
10496
10731
|
var TOP_LINKS = 10;
|
|
10497
|
-
var RECALL_TOP_LINKS = 5;
|
|
10498
10732
|
function recencyDecay(modifiedDate) {
|
|
10499
10733
|
if (!modifiedDate) return 0.5;
|
|
10500
10734
|
const daysSince = (Date.now() - modifiedDate.getTime()) / (1e3 * 60 * 60 * 24);
|
|
@@ -10584,6 +10818,63 @@ function rankBacklinks(backlinks, notePath, index, stateDb2, maxLinks = TOP_LINK
|
|
|
10584
10818
|
return out;
|
|
10585
10819
|
}).sort((a, b) => (b.weight ?? 1) - (a.weight ?? 1)).slice(0, maxLinks);
|
|
10586
10820
|
}
|
|
10821
|
+
var COMPACT_OUTLINK_NAMES = 10;
|
|
10822
|
+
function enrichResultCompact(result, index, stateDb2, opts) {
|
|
10823
|
+
const note = index.notes.get(result.path);
|
|
10824
|
+
const normalizedPath = result.path.toLowerCase().replace(/\.md$/, "");
|
|
10825
|
+
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
10826
|
+
const enriched = {
|
|
10827
|
+
path: result.path,
|
|
10828
|
+
title: result.title
|
|
10829
|
+
};
|
|
10830
|
+
if (result.snippet) {
|
|
10831
|
+
enriched.snippet = result.snippet;
|
|
10832
|
+
} else {
|
|
10833
|
+
const preview = getContentPreview(result.path);
|
|
10834
|
+
if (preview) enriched.snippet = preview;
|
|
10835
|
+
}
|
|
10836
|
+
if (note) {
|
|
10837
|
+
enriched.backlink_count = backlinks.length;
|
|
10838
|
+
enriched.modified = note.modified.toISOString();
|
|
10839
|
+
if (note.tags.length > 0) enriched.tags = note.tags;
|
|
10840
|
+
if (note.outlinks.length > 0) {
|
|
10841
|
+
enriched.outlink_names = getOutlinkNames(note.outlinks, result.path, index, stateDb2, COMPACT_OUTLINK_NAMES);
|
|
10842
|
+
}
|
|
10843
|
+
}
|
|
10844
|
+
if (stateDb2) {
|
|
10845
|
+
try {
|
|
10846
|
+
const entity = getEntityByName2(stateDb2, result.title);
|
|
10847
|
+
if (entity) {
|
|
10848
|
+
enriched.category = entity.category;
|
|
10849
|
+
enriched.hub_score = entity.hubScore;
|
|
10850
|
+
if (!enriched.snippet && entity.description) {
|
|
10851
|
+
enriched.snippet = entity.description;
|
|
10852
|
+
}
|
|
10853
|
+
}
|
|
10854
|
+
} catch {
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10857
|
+
if (opts?.via) enriched.via = opts.via;
|
|
10858
|
+
if (opts?.hop) enriched.hop = opts.hop;
|
|
10859
|
+
return enriched;
|
|
10860
|
+
}
|
|
10861
|
+
function getOutlinkNames(outlinks, notePath, index, stateDb2, max) {
|
|
10862
|
+
const weightMap = /* @__PURE__ */ new Map();
|
|
10863
|
+
if (stateDb2) {
|
|
10864
|
+
try {
|
|
10865
|
+
const rows = stateDb2.db.prepare(
|
|
10866
|
+
"SELECT target, weight, weight_updated_at FROM note_links WHERE note_path = ?"
|
|
10867
|
+
).all(notePath);
|
|
10868
|
+
for (const row of rows) {
|
|
10869
|
+
const daysSince = row.weight_updated_at ? (Date.now() - row.weight_updated_at) / (1e3 * 60 * 60 * 24) : 0;
|
|
10870
|
+
const decay = Math.max(0.1, 1 - daysSince / 180);
|
|
10871
|
+
weightMap.set(row.target, row.weight * decay);
|
|
10872
|
+
}
|
|
10873
|
+
} catch {
|
|
10874
|
+
}
|
|
10875
|
+
}
|
|
10876
|
+
return outlinks.map((l) => ({ name: l.target, weight: weightMap.get(l.target.toLowerCase()) ?? 1 })).sort((a, b) => b.weight - a.weight).slice(0, max).map((l) => l.name);
|
|
10877
|
+
}
|
|
10587
10878
|
function enrichResult(result, index, stateDb2) {
|
|
10588
10879
|
const note = index.notes.get(result.path);
|
|
10589
10880
|
const normalizedPath = result.path.toLowerCase().replace(/\.md$/, "");
|
|
@@ -10651,7 +10942,7 @@ function enrichResultLight(result, index, stateDb2) {
|
|
|
10651
10942
|
}
|
|
10652
10943
|
return enriched;
|
|
10653
10944
|
}
|
|
10654
|
-
function
|
|
10945
|
+
function enrichEntityCompact(entityName, stateDb2, index) {
|
|
10655
10946
|
const enriched = {};
|
|
10656
10947
|
if (stateDb2) {
|
|
10657
10948
|
try {
|
|
@@ -10673,36 +10964,27 @@ function enrichEntityResult(entityName, stateDb2, index) {
|
|
|
10673
10964
|
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
10674
10965
|
enriched.backlink_count = backlinks.length;
|
|
10675
10966
|
if (note) {
|
|
10676
|
-
enriched.outlink_count = note.outlinks.length;
|
|
10677
10967
|
if (note.tags.length > 0) enriched.tags = note.tags;
|
|
10678
|
-
if (backlinks.length > 0) {
|
|
10679
|
-
enriched.top_backlinks = rankBacklinks(backlinks, entityPath, index, stateDb2, RECALL_TOP_LINKS);
|
|
10680
|
-
}
|
|
10681
10968
|
if (note.outlinks.length > 0) {
|
|
10682
|
-
enriched.
|
|
10969
|
+
enriched.outlink_names = getOutlinkNames(note.outlinks, entityPath, index, stateDb2, COMPACT_OUTLINK_NAMES);
|
|
10683
10970
|
}
|
|
10684
10971
|
}
|
|
10685
10972
|
}
|
|
10686
10973
|
}
|
|
10687
10974
|
return enriched;
|
|
10688
10975
|
}
|
|
10689
|
-
function
|
|
10976
|
+
function enrichNoteCompact(notePath, stateDb2, index) {
|
|
10690
10977
|
const enriched = {};
|
|
10691
10978
|
if (!index) return enriched;
|
|
10692
10979
|
const note = index.notes.get(notePath);
|
|
10693
10980
|
if (!note) return enriched;
|
|
10694
10981
|
const normalizedPath = notePath.toLowerCase().replace(/\.md$/, "");
|
|
10695
10982
|
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
10696
|
-
enriched.frontmatter = note.frontmatter;
|
|
10697
10983
|
if (note.tags.length > 0) enriched.tags = note.tags;
|
|
10698
10984
|
enriched.backlink_count = backlinks.length;
|
|
10699
|
-
enriched.outlink_count = note.outlinks.length;
|
|
10700
10985
|
enriched.modified = note.modified.toISOString();
|
|
10701
|
-
if (backlinks.length > 0) {
|
|
10702
|
-
enriched.top_backlinks = rankBacklinks(backlinks, notePath, index, stateDb2, RECALL_TOP_LINKS);
|
|
10703
|
-
}
|
|
10704
10986
|
if (note.outlinks.length > 0) {
|
|
10705
|
-
enriched.
|
|
10987
|
+
enriched.outlink_names = getOutlinkNames(note.outlinks, notePath, index, stateDb2, COMPACT_OUTLINK_NAMES);
|
|
10706
10988
|
}
|
|
10707
10989
|
if (stateDb2) {
|
|
10708
10990
|
try {
|
|
@@ -10717,8 +10999,132 @@ function enrichNoteResult(notePath, stateDb2, index) {
|
|
|
10717
10999
|
return enriched;
|
|
10718
11000
|
}
|
|
10719
11001
|
|
|
11002
|
+
// src/core/read/multihop.ts
|
|
11003
|
+
import { getEntityByName as getEntityByName3, searchEntities } from "@velvetmonkey/vault-core";
|
|
11004
|
+
var DEFAULT_CONFIG2 = {
|
|
11005
|
+
maxParents: 10,
|
|
11006
|
+
maxHops: 2,
|
|
11007
|
+
maxOutlinksPerHop: 10,
|
|
11008
|
+
maxBackfill: 10
|
|
11009
|
+
};
|
|
11010
|
+
function multiHopBackfill(primaryResults, index, stateDb2, config = {}) {
|
|
11011
|
+
const cfg = { ...DEFAULT_CONFIG2, ...config };
|
|
11012
|
+
const seen = new Set(primaryResults.map((r) => r.path).filter(Boolean));
|
|
11013
|
+
const candidates = [];
|
|
11014
|
+
const hop1Results = [];
|
|
11015
|
+
for (const primary of primaryResults.slice(0, cfg.maxParents)) {
|
|
11016
|
+
const primaryPath = primary.path;
|
|
11017
|
+
if (!primaryPath) continue;
|
|
11018
|
+
const note = index.notes.get(primaryPath);
|
|
11019
|
+
if (!note) continue;
|
|
11020
|
+
for (const outlink of note.outlinks.slice(0, cfg.maxOutlinksPerHop)) {
|
|
11021
|
+
const targetPath = index.entities.get(outlink.target.toLowerCase());
|
|
11022
|
+
if (!targetPath || seen.has(targetPath)) continue;
|
|
11023
|
+
seen.add(targetPath);
|
|
11024
|
+
const targetNote = index.notes.get(targetPath);
|
|
11025
|
+
const title = targetNote?.title ?? outlink.target;
|
|
11026
|
+
hop1Results.push({ path: targetPath, title, via: primaryPath });
|
|
11027
|
+
}
|
|
11028
|
+
}
|
|
11029
|
+
for (const h1 of hop1Results) {
|
|
11030
|
+
const enriched = enrichResultCompact(
|
|
11031
|
+
{ path: h1.path, title: h1.title },
|
|
11032
|
+
index,
|
|
11033
|
+
stateDb2,
|
|
11034
|
+
{ via: h1.via, hop: 1 }
|
|
11035
|
+
);
|
|
11036
|
+
const score = scoreCandidate(h1.path, index, stateDb2);
|
|
11037
|
+
candidates.push({ result: enriched, score });
|
|
11038
|
+
}
|
|
11039
|
+
if (cfg.maxHops >= 2) {
|
|
11040
|
+
for (const h1 of hop1Results) {
|
|
11041
|
+
const note = index.notes.get(h1.path);
|
|
11042
|
+
if (!note) continue;
|
|
11043
|
+
for (const outlink of note.outlinks.slice(0, cfg.maxOutlinksPerHop)) {
|
|
11044
|
+
const targetPath = index.entities.get(outlink.target.toLowerCase());
|
|
11045
|
+
if (!targetPath || seen.has(targetPath)) continue;
|
|
11046
|
+
seen.add(targetPath);
|
|
11047
|
+
const targetNote = index.notes.get(targetPath);
|
|
11048
|
+
const title = targetNote?.title ?? outlink.target;
|
|
11049
|
+
const enriched = enrichResultCompact(
|
|
11050
|
+
{ path: targetPath, title },
|
|
11051
|
+
index,
|
|
11052
|
+
stateDb2,
|
|
11053
|
+
{ via: h1.path, hop: 2 }
|
|
11054
|
+
);
|
|
11055
|
+
const score = scoreCandidate(targetPath, index, stateDb2);
|
|
11056
|
+
candidates.push({ result: enriched, score });
|
|
11057
|
+
}
|
|
11058
|
+
}
|
|
11059
|
+
}
|
|
11060
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
11061
|
+
return candidates.slice(0, cfg.maxBackfill).map((c) => c.result);
|
|
11062
|
+
}
|
|
11063
|
+
function scoreCandidate(path33, index, stateDb2) {
|
|
11064
|
+
const note = index.notes.get(path33);
|
|
11065
|
+
const decay = recencyDecay(note?.modified);
|
|
11066
|
+
let hubScore = 1;
|
|
11067
|
+
if (stateDb2) {
|
|
11068
|
+
try {
|
|
11069
|
+
const title = note?.title ?? path33.replace(/\.md$/, "").split("/").pop() ?? "";
|
|
11070
|
+
const entity = getEntityByName3(stateDb2, title);
|
|
11071
|
+
if (entity) hubScore = entity.hubScore ?? 1;
|
|
11072
|
+
} catch {
|
|
11073
|
+
}
|
|
11074
|
+
}
|
|
11075
|
+
return hubScore * decay;
|
|
11076
|
+
}
|
|
11077
|
+
function extractExpansionTerms(results, originalQuery, index) {
|
|
11078
|
+
const queryLower = originalQuery.toLowerCase();
|
|
11079
|
+
const terms = /* @__PURE__ */ new Set();
|
|
11080
|
+
for (const r of results.slice(0, 5)) {
|
|
11081
|
+
const outlinks = r.outlink_names;
|
|
11082
|
+
if (outlinks) {
|
|
11083
|
+
for (const name of outlinks) {
|
|
11084
|
+
if (!queryLower.includes(name.toLowerCase()) && index.entities.has(name.toLowerCase())) {
|
|
11085
|
+
terms.add(name);
|
|
11086
|
+
}
|
|
11087
|
+
}
|
|
11088
|
+
}
|
|
11089
|
+
const snippet = r.snippet;
|
|
11090
|
+
if (snippet) {
|
|
11091
|
+
const matches = snippet.match(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g);
|
|
11092
|
+
if (matches) {
|
|
11093
|
+
for (const wl of matches) {
|
|
11094
|
+
const name = wl.replace(/\[\[|\]\]/g, "").split("|")[0];
|
|
11095
|
+
if (!queryLower.includes(name.toLowerCase()) && index.entities.has(name.toLowerCase())) {
|
|
11096
|
+
terms.add(name);
|
|
11097
|
+
}
|
|
11098
|
+
}
|
|
11099
|
+
}
|
|
11100
|
+
}
|
|
11101
|
+
}
|
|
11102
|
+
return Array.from(terms).slice(0, 10);
|
|
11103
|
+
}
|
|
11104
|
+
function expandQuery(expansionTerms, primaryResults, index, stateDb2) {
|
|
11105
|
+
if (!stateDb2 || expansionTerms.length === 0) return [];
|
|
11106
|
+
const seen = new Set(primaryResults.map((r) => r.path).filter(Boolean));
|
|
11107
|
+
const results = [];
|
|
11108
|
+
for (const term of expansionTerms) {
|
|
11109
|
+
try {
|
|
11110
|
+
const entities = searchEntities(stateDb2, term, 3);
|
|
11111
|
+
for (const entity of entities) {
|
|
11112
|
+
if (!entity.path || seen.has(entity.path)) continue;
|
|
11113
|
+
seen.add(entity.path);
|
|
11114
|
+
results.push(enrichResultCompact(
|
|
11115
|
+
{ path: entity.path, title: entity.name },
|
|
11116
|
+
index,
|
|
11117
|
+
stateDb2,
|
|
11118
|
+
{ via: "query_expansion" }
|
|
11119
|
+
));
|
|
11120
|
+
}
|
|
11121
|
+
} catch {
|
|
11122
|
+
}
|
|
11123
|
+
}
|
|
11124
|
+
return results;
|
|
11125
|
+
}
|
|
11126
|
+
|
|
10720
11127
|
// src/tools/read/query.ts
|
|
10721
|
-
init_wikilinkFeedback();
|
|
10722
11128
|
function matchesFrontmatter(note, where) {
|
|
10723
11129
|
for (const [key, value] of Object.entries(where)) {
|
|
10724
11130
|
const noteValue = note.frontmatter[key];
|
|
@@ -10899,7 +11305,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
|
10899
11305
|
const stateDbEntity = getStateDb2();
|
|
10900
11306
|
if (stateDbEntity) {
|
|
10901
11307
|
try {
|
|
10902
|
-
entityResults =
|
|
11308
|
+
entityResults = searchEntities2(stateDbEntity, query, limit);
|
|
10903
11309
|
} catch {
|
|
10904
11310
|
}
|
|
10905
11311
|
}
|
|
@@ -10971,46 +11377,22 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
|
10971
11377
|
scored.sort((a, b) => b.rrf_score - a.rrf_score);
|
|
10972
11378
|
const filtered = applyFolderFilter(scored);
|
|
10973
11379
|
const stateDb2 = getStateDb2();
|
|
10974
|
-
const
|
|
10975
|
-
...(
|
|
11380
|
+
const results2 = filtered.slice(0, limit).map((item) => ({
|
|
11381
|
+
...enrichResultCompact({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
|
|
10976
11382
|
rrf_score: item.rrf_score,
|
|
10977
11383
|
in_fts5: item.in_fts5,
|
|
10978
11384
|
in_semantic: item.in_semantic,
|
|
10979
11385
|
in_entity: item.in_entity
|
|
10980
11386
|
}));
|
|
10981
|
-
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
const rPath = r.path;
|
|
10986
|
-
if (!rPath) continue;
|
|
10987
|
-
try {
|
|
10988
|
-
const outlinks = getStoredNoteLinks(stateDb2, rPath);
|
|
10989
|
-
for (const target of outlinks) {
|
|
10990
|
-
const entityRow = stateDb2.db.prepare(
|
|
10991
|
-
"SELECT path FROM entities WHERE name_lower = ?"
|
|
10992
|
-
).get(target);
|
|
10993
|
-
if (entityRow?.path && !existingPaths.has(entityRow.path)) {
|
|
10994
|
-
existingPaths.add(entityRow.path);
|
|
10995
|
-
backfill.push({
|
|
10996
|
-
...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2),
|
|
10997
|
-
rrf_score: 0,
|
|
10998
|
-
in_fts5: false,
|
|
10999
|
-
in_semantic: false,
|
|
11000
|
-
in_entity: false
|
|
11001
|
-
});
|
|
11002
|
-
}
|
|
11003
|
-
}
|
|
11004
|
-
} catch {
|
|
11005
|
-
}
|
|
11006
|
-
}
|
|
11007
|
-
results.push(...backfill.slice(0, limit - results.length));
|
|
11008
|
-
}
|
|
11387
|
+
const hopResults2 = multiHopBackfill(results2, index, stateDb2, { maxBackfill: limit });
|
|
11388
|
+
const expansionTerms2 = extractExpansionTerms(results2, query, index);
|
|
11389
|
+
const expansionResults2 = expandQuery(expansionTerms2, [...results2, ...hopResults2], index, stateDb2);
|
|
11390
|
+
results2.push(...hopResults2, ...expansionResults2);
|
|
11009
11391
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
11010
11392
|
method: "hybrid",
|
|
11011
11393
|
query,
|
|
11012
11394
|
total_results: filtered.length,
|
|
11013
|
-
results
|
|
11395
|
+
results: results2
|
|
11014
11396
|
}, null, 2) }] };
|
|
11015
11397
|
} catch (err) {
|
|
11016
11398
|
console.error("[Semantic] Hybrid search failed, falling back to FTS5:", err instanceof Error ? err.message : err);
|
|
@@ -11027,49 +11409,33 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
|
11027
11409
|
const filtered = applyFolderFilter(mergedItems);
|
|
11028
11410
|
const stateDb2 = getStateDb2();
|
|
11029
11411
|
const sliced = filtered.slice(0, limit);
|
|
11030
|
-
const
|
|
11031
|
-
...(
|
|
11412
|
+
const results2 = sliced.map((item) => ({
|
|
11413
|
+
...enrichResultCompact({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
|
|
11032
11414
|
..."in_fts5" in item ? { in_fts5: true } : { in_entity: true }
|
|
11033
11415
|
}));
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
const rPath = r.path;
|
|
11039
|
-
if (!rPath) continue;
|
|
11040
|
-
try {
|
|
11041
|
-
const outlinks = getStoredNoteLinks(stateDb2, rPath);
|
|
11042
|
-
for (const target of outlinks) {
|
|
11043
|
-
const entityRow = stateDb2.db.prepare(
|
|
11044
|
-
"SELECT path FROM entities WHERE name_lower = ?"
|
|
11045
|
-
).get(target);
|
|
11046
|
-
if (entityRow?.path && !existingPaths.has(entityRow.path)) {
|
|
11047
|
-
existingPaths.add(entityRow.path);
|
|
11048
|
-
backfill.push({
|
|
11049
|
-
...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2)
|
|
11050
|
-
});
|
|
11051
|
-
}
|
|
11052
|
-
}
|
|
11053
|
-
} catch {
|
|
11054
|
-
}
|
|
11055
|
-
}
|
|
11056
|
-
results.push(...backfill.slice(0, limit - results.length));
|
|
11057
|
-
}
|
|
11416
|
+
const hopResults2 = multiHopBackfill(results2, index, stateDb2, { maxBackfill: limit });
|
|
11417
|
+
const expansionTerms2 = extractExpansionTerms(results2, query, index);
|
|
11418
|
+
const expansionResults2 = expandQuery(expansionTerms2, [...results2, ...hopResults2], index, stateDb2);
|
|
11419
|
+
results2.push(...hopResults2, ...expansionResults2);
|
|
11058
11420
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
11059
11421
|
method: "fts5",
|
|
11060
11422
|
query,
|
|
11061
11423
|
total_results: filtered.length,
|
|
11062
|
-
results
|
|
11424
|
+
results: results2
|
|
11063
11425
|
}, null, 2) }] };
|
|
11064
11426
|
}
|
|
11065
11427
|
const stateDbFts = getStateDb2();
|
|
11066
11428
|
const fts5Filtered = applyFolderFilter(fts5Results);
|
|
11067
|
-
const
|
|
11429
|
+
const results = fts5Filtered.map((r) => ({ ...enrichResultCompact({ path: r.path, title: r.title, snippet: r.snippet }, index, stateDbFts), in_fts5: true }));
|
|
11430
|
+
const hopResults = multiHopBackfill(results, index, stateDbFts, { maxBackfill: limit });
|
|
11431
|
+
const expansionTerms = extractExpansionTerms(results, query, index);
|
|
11432
|
+
const expansionResults = expandQuery(expansionTerms, [...results, ...hopResults], index, stateDbFts);
|
|
11433
|
+
results.push(...hopResults, ...expansionResults);
|
|
11068
11434
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
11069
11435
|
method: "fts5",
|
|
11070
11436
|
query,
|
|
11071
|
-
total_results:
|
|
11072
|
-
results
|
|
11437
|
+
total_results: results.length,
|
|
11438
|
+
results
|
|
11073
11439
|
}, null, 2) }] };
|
|
11074
11440
|
}
|
|
11075
11441
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Provide a query or metadata filters (where, has_tag, folder, etc.)" }, null, 2) }] };
|
|
@@ -11795,7 +12161,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
11795
12161
|
}
|
|
11796
12162
|
|
|
11797
12163
|
// src/tools/read/primitives.ts
|
|
11798
|
-
import { getEntityByName as
|
|
12164
|
+
import { getEntityByName as getEntityByName4 } from "@velvetmonkey/vault-core";
|
|
11799
12165
|
function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb2 = () => null) {
|
|
11800
12166
|
server2.registerTool(
|
|
11801
12167
|
"get_note_structure",
|
|
@@ -11838,7 +12204,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
|
|
|
11838
12204
|
const stateDb2 = getStateDb2();
|
|
11839
12205
|
if (stateDb2 && note) {
|
|
11840
12206
|
try {
|
|
11841
|
-
const entity =
|
|
12207
|
+
const entity = getEntityByName4(stateDb2, note.title);
|
|
11842
12208
|
if (entity) {
|
|
11843
12209
|
enriched.category = entity.category;
|
|
11844
12210
|
enriched.hub_score = entity.hubScore;
|
|
@@ -12899,9 +13265,9 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb2
|
|
|
12899
13265
|
"graph_analysis",
|
|
12900
13266
|
{
|
|
12901
13267
|
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)',
|
|
13268
|
+
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
13269
|
inputSchema: {
|
|
12904
|
-
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale", "immature", "emerging_hubs"]).describe("Type of graph analysis to perform"),
|
|
13270
|
+
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale", "immature", "emerging_hubs", "centrality", "cycles"]).describe("Type of graph analysis to perform"),
|
|
12905
13271
|
folder: z8.string().optional().describe("Limit to notes in this folder (orphans, dead_ends, sources, immature)"),
|
|
12906
13272
|
min_links: z8.coerce.number().default(5).describe("Minimum total connections for hubs"),
|
|
12907
13273
|
min_backlinks: z8.coerce.number().default(1).describe("Minimum backlinks (dead_ends, stale)"),
|
|
@@ -13100,6 +13466,28 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb2
|
|
|
13100
13466
|
}, null, 2) }]
|
|
13101
13467
|
};
|
|
13102
13468
|
}
|
|
13469
|
+
case "centrality": {
|
|
13470
|
+
const results = computeCentralityMetrics(index, limit);
|
|
13471
|
+
const paginated = results.slice(offset, offset + limit);
|
|
13472
|
+
return {
|
|
13473
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
13474
|
+
analysis: "centrality",
|
|
13475
|
+
total_count: results.length,
|
|
13476
|
+
returned_count: paginated.length,
|
|
13477
|
+
notes: paginated
|
|
13478
|
+
}, null, 2) }]
|
|
13479
|
+
};
|
|
13480
|
+
}
|
|
13481
|
+
case "cycles": {
|
|
13482
|
+
const cycles = detectCycles(index, 10, limit);
|
|
13483
|
+
return {
|
|
13484
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
13485
|
+
analysis: "cycles",
|
|
13486
|
+
total_count: cycles.length,
|
|
13487
|
+
cycles
|
|
13488
|
+
}, null, 2) }]
|
|
13489
|
+
};
|
|
13490
|
+
}
|
|
13103
13491
|
}
|
|
13104
13492
|
}
|
|
13105
13493
|
);
|
|
@@ -19068,13 +19456,13 @@ function registerRecallTools(server2, getStateDb2, getVaultPath, getIndex) {
|
|
|
19068
19456
|
description: e.content,
|
|
19069
19457
|
score: Math.round(e.score * 10) / 10,
|
|
19070
19458
|
breakdown: e.breakdown,
|
|
19071
|
-
...
|
|
19459
|
+
...enrichEntityCompact(e.id, stateDb2, index)
|
|
19072
19460
|
})),
|
|
19073
19461
|
notes: notes.map((n) => ({
|
|
19074
19462
|
path: n.id,
|
|
19075
19463
|
snippet: n.content,
|
|
19076
19464
|
score: Math.round(n.score * 10) / 10,
|
|
19077
|
-
...
|
|
19465
|
+
...enrichNoteCompact(n.id, stateDb2, index)
|
|
19078
19466
|
})),
|
|
19079
19467
|
memories: memories.map((m) => ({
|
|
19080
19468
|
key: m.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.123",
|
|
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",
|