@velvetmonkey/flywheel-memory 2.0.41 → 2.0.43
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 +296 -154
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3058,7 +3058,7 @@ var DEFAULT_WATCHER_CONFIG = {
|
|
|
3058
3058
|
flushMs: 1e3,
|
|
3059
3059
|
batchSize: 50,
|
|
3060
3060
|
usePolling: false,
|
|
3061
|
-
pollInterval:
|
|
3061
|
+
pollInterval: 1e4
|
|
3062
3062
|
};
|
|
3063
3063
|
function parseWatcherConfig() {
|
|
3064
3064
|
const debounceMs = parseInt(process.env.FLYWHEEL_DEBOUNCE_MS || "");
|
|
@@ -3703,7 +3703,7 @@ function trackWikilinkApplications(stateDb2, notePath, entities) {
|
|
|
3703
3703
|
`);
|
|
3704
3704
|
const transaction = stateDb2.db.transaction(() => {
|
|
3705
3705
|
for (const entity of entities) {
|
|
3706
|
-
upsert.run(entity
|
|
3706
|
+
upsert.run(entity, notePath);
|
|
3707
3707
|
}
|
|
3708
3708
|
});
|
|
3709
3709
|
transaction();
|
|
@@ -3776,7 +3776,7 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
|
|
|
3776
3776
|
);
|
|
3777
3777
|
const transaction = stateDb2.db.transaction(() => {
|
|
3778
3778
|
for (const entity of tracked) {
|
|
3779
|
-
if (!currentLinks.has(entity)) {
|
|
3779
|
+
if (!currentLinks.has(entity.toLowerCase())) {
|
|
3780
3780
|
recordFeedback(stateDb2, entity, "implicit:removed", notePath, false);
|
|
3781
3781
|
markRemoved.run(entity, notePath);
|
|
3782
3782
|
removed.push(entity);
|
|
@@ -5372,16 +5372,160 @@ function getCooccurrenceBoost(entityName, matchedEntities, cooccurrenceIndex2, r
|
|
|
5372
5372
|
return Math.min(boost, MAX_COOCCURRENCE_BOOST);
|
|
5373
5373
|
}
|
|
5374
5374
|
|
|
5375
|
-
// src/core/write/
|
|
5375
|
+
// src/core/write/edgeWeights.ts
|
|
5376
5376
|
var moduleStateDb4 = null;
|
|
5377
|
-
function
|
|
5377
|
+
function setEdgeWeightStateDb(stateDb2) {
|
|
5378
5378
|
moduleStateDb4 = stateDb2;
|
|
5379
|
+
}
|
|
5380
|
+
function buildPathToTargetsMap(stateDb2) {
|
|
5381
|
+
const map = /* @__PURE__ */ new Map();
|
|
5382
|
+
const rows = stateDb2.db.prepare(
|
|
5383
|
+
"SELECT path, name_lower, aliases_json FROM entities"
|
|
5384
|
+
).all();
|
|
5385
|
+
for (const row of rows) {
|
|
5386
|
+
const targets = /* @__PURE__ */ new Set();
|
|
5387
|
+
targets.add(row.name_lower);
|
|
5388
|
+
if (row.aliases_json) {
|
|
5389
|
+
try {
|
|
5390
|
+
const aliases = JSON.parse(row.aliases_json);
|
|
5391
|
+
for (const alias of aliases) {
|
|
5392
|
+
targets.add(alias.toLowerCase());
|
|
5393
|
+
}
|
|
5394
|
+
} catch {
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
map.set(row.path, targets);
|
|
5398
|
+
}
|
|
5399
|
+
return map;
|
|
5400
|
+
}
|
|
5401
|
+
function pathToFallbackTarget(filePath) {
|
|
5402
|
+
return filePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? filePath.toLowerCase();
|
|
5403
|
+
}
|
|
5404
|
+
function recomputeEdgeWeights(stateDb2) {
|
|
5405
|
+
const start = Date.now();
|
|
5406
|
+
const edges = stateDb2.db.prepare(
|
|
5407
|
+
"SELECT note_path, target FROM note_links"
|
|
5408
|
+
).all();
|
|
5409
|
+
if (edges.length === 0) {
|
|
5410
|
+
return { edges_updated: 0, duration_ms: Date.now() - start, total_weighted: 0, avg_weight: 0, strong_count: 0 };
|
|
5411
|
+
}
|
|
5412
|
+
const survivalMap = /* @__PURE__ */ new Map();
|
|
5413
|
+
const historyRows = stateDb2.db.prepare(
|
|
5414
|
+
"SELECT note_path, target, edits_survived FROM note_link_history"
|
|
5415
|
+
).all();
|
|
5416
|
+
for (const row of historyRows) {
|
|
5417
|
+
survivalMap.set(`${row.note_path}\0${row.target}`, row.edits_survived);
|
|
5418
|
+
}
|
|
5419
|
+
const pathToTargets = buildPathToTargetsMap(stateDb2);
|
|
5420
|
+
const targetToPaths = /* @__PURE__ */ new Map();
|
|
5421
|
+
for (const [entityPath, targets] of pathToTargets) {
|
|
5422
|
+
for (const target of targets) {
|
|
5423
|
+
let paths = targetToPaths.get(target);
|
|
5424
|
+
if (!paths) {
|
|
5425
|
+
paths = /* @__PURE__ */ new Set();
|
|
5426
|
+
targetToPaths.set(target, paths);
|
|
5427
|
+
}
|
|
5428
|
+
paths.add(entityPath);
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
const sessionRows = stateDb2.db.prepare(
|
|
5432
|
+
`SELECT session_id, note_paths FROM tool_invocations
|
|
5433
|
+
WHERE note_paths IS NOT NULL AND note_paths != '[]'`
|
|
5434
|
+
).all();
|
|
5435
|
+
const sessionPaths = /* @__PURE__ */ new Map();
|
|
5436
|
+
for (const row of sessionRows) {
|
|
5437
|
+
try {
|
|
5438
|
+
const paths = JSON.parse(row.note_paths);
|
|
5439
|
+
if (!Array.isArray(paths) || paths.length === 0) continue;
|
|
5440
|
+
let existing = sessionPaths.get(row.session_id);
|
|
5441
|
+
if (!existing) {
|
|
5442
|
+
existing = /* @__PURE__ */ new Set();
|
|
5443
|
+
sessionPaths.set(row.session_id, existing);
|
|
5444
|
+
}
|
|
5445
|
+
for (const p of paths) {
|
|
5446
|
+
existing.add(p);
|
|
5447
|
+
}
|
|
5448
|
+
} catch {
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
const coSessionCount = /* @__PURE__ */ new Map();
|
|
5452
|
+
const sourceActivityCount = /* @__PURE__ */ new Map();
|
|
5453
|
+
for (const [, paths] of sessionPaths) {
|
|
5454
|
+
const sessionTargets = /* @__PURE__ */ new Set();
|
|
5455
|
+
for (const p of paths) {
|
|
5456
|
+
const targets = pathToTargets.get(p);
|
|
5457
|
+
if (targets) {
|
|
5458
|
+
for (const t of targets) sessionTargets.add(t);
|
|
5459
|
+
} else {
|
|
5460
|
+
sessionTargets.add(pathToFallbackTarget(p));
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
for (const edge of edges) {
|
|
5464
|
+
if (paths.has(edge.note_path)) {
|
|
5465
|
+
const srcKey = edge.note_path;
|
|
5466
|
+
sourceActivityCount.set(srcKey, (sourceActivityCount.get(srcKey) ?? 0) + 1);
|
|
5467
|
+
if (sessionTargets.has(edge.target)) {
|
|
5468
|
+
const edgeKey = `${edge.note_path}\0${edge.target}`;
|
|
5469
|
+
coSessionCount.set(edgeKey, (coSessionCount.get(edgeKey) ?? 0) + 1);
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
const now = Date.now();
|
|
5475
|
+
const update = stateDb2.db.prepare(
|
|
5476
|
+
"UPDATE note_links SET weight = ?, weight_updated_at = ? WHERE note_path = ? AND target = ?"
|
|
5477
|
+
);
|
|
5478
|
+
const tx = stateDb2.db.transaction(() => {
|
|
5479
|
+
for (const edge of edges) {
|
|
5480
|
+
const edgeKey = `${edge.note_path}\0${edge.target}`;
|
|
5481
|
+
const editsSurvived = survivalMap.get(edgeKey) ?? 0;
|
|
5482
|
+
const coSessions = coSessionCount.get(edgeKey) ?? 0;
|
|
5483
|
+
const sourceAccess = sourceActivityCount.get(edge.note_path) ?? 0;
|
|
5484
|
+
const weight = 1 + editsSurvived * 0.5 + Math.min(coSessions * 0.5, 3) + Math.min(sourceAccess * 0.2, 2);
|
|
5485
|
+
update.run(Math.round(weight * 1e3) / 1e3, now, edge.note_path, edge.target);
|
|
5486
|
+
}
|
|
5487
|
+
});
|
|
5488
|
+
tx();
|
|
5489
|
+
const stats = stateDb2.db.prepare(`
|
|
5490
|
+
SELECT
|
|
5491
|
+
COUNT(*) as total_weighted,
|
|
5492
|
+
AVG(weight) as avg_weight,
|
|
5493
|
+
SUM(CASE WHEN weight > 3.0 THEN 1 ELSE 0 END) as strong_count
|
|
5494
|
+
FROM note_links
|
|
5495
|
+
WHERE weight > 1.0
|
|
5496
|
+
`).get();
|
|
5497
|
+
return {
|
|
5498
|
+
edges_updated: edges.length,
|
|
5499
|
+
duration_ms: Date.now() - start,
|
|
5500
|
+
total_weighted: stats?.total_weighted ?? 0,
|
|
5501
|
+
avg_weight: Math.round((stats?.avg_weight ?? 0) * 100) / 100,
|
|
5502
|
+
strong_count: stats?.strong_count ?? 0
|
|
5503
|
+
};
|
|
5504
|
+
}
|
|
5505
|
+
function getEntityEdgeWeightMap(stateDb2) {
|
|
5506
|
+
const rows = stateDb2.db.prepare(`
|
|
5507
|
+
SELECT LOWER(target) as target_lower, AVG(weight) as avg_weight
|
|
5508
|
+
FROM note_links
|
|
5509
|
+
WHERE weight > 1.0
|
|
5510
|
+
GROUP BY LOWER(target)
|
|
5511
|
+
`).all();
|
|
5512
|
+
const map = /* @__PURE__ */ new Map();
|
|
5513
|
+
for (const row of rows) {
|
|
5514
|
+
map.set(row.target_lower, row.avg_weight);
|
|
5515
|
+
}
|
|
5516
|
+
return map;
|
|
5517
|
+
}
|
|
5518
|
+
|
|
5519
|
+
// src/core/write/wikilinks.ts
|
|
5520
|
+
var moduleStateDb5 = null;
|
|
5521
|
+
function setWriteStateDb(stateDb2) {
|
|
5522
|
+
moduleStateDb5 = stateDb2;
|
|
5379
5523
|
setGitStateDb(stateDb2);
|
|
5380
5524
|
setHintsStateDb(stateDb2);
|
|
5381
5525
|
setRecencyStateDb(stateDb2);
|
|
5382
5526
|
}
|
|
5383
5527
|
function getWriteStateDb() {
|
|
5384
|
-
return
|
|
5528
|
+
return moduleStateDb5;
|
|
5385
5529
|
}
|
|
5386
5530
|
var moduleConfig = null;
|
|
5387
5531
|
var ALL_IMPLICIT_PATTERNS = ["proper-nouns", "single-caps", "quoted-terms", "camel-case", "acronyms"];
|
|
@@ -5436,9 +5580,9 @@ var DEFAULT_EXCLUDE_FOLDERS = [
|
|
|
5436
5580
|
];
|
|
5437
5581
|
async function initializeEntityIndex(vaultPath2) {
|
|
5438
5582
|
try {
|
|
5439
|
-
if (
|
|
5583
|
+
if (moduleStateDb5) {
|
|
5440
5584
|
try {
|
|
5441
|
-
const dbIndex = getEntityIndexFromDb(
|
|
5585
|
+
const dbIndex = getEntityIndexFromDb(moduleStateDb5);
|
|
5442
5586
|
if (dbIndex._metadata.total_entities > 0) {
|
|
5443
5587
|
entityIndex = dbIndex;
|
|
5444
5588
|
indexReady = true;
|
|
@@ -5466,9 +5610,9 @@ async function rebuildIndex(vaultPath2) {
|
|
|
5466
5610
|
lastLoadedAt = Date.now();
|
|
5467
5611
|
const entityDuration = Date.now() - startTime;
|
|
5468
5612
|
console.error(`[Flywheel] Entity index built: ${entityIndex._metadata.total_entities} entities in ${entityDuration}ms`);
|
|
5469
|
-
if (
|
|
5613
|
+
if (moduleStateDb5) {
|
|
5470
5614
|
try {
|
|
5471
|
-
|
|
5615
|
+
moduleStateDb5.replaceAllEntities(entityIndex);
|
|
5472
5616
|
console.error(`[Flywheel] Saved entities to StateDb`);
|
|
5473
5617
|
} catch (e) {
|
|
5474
5618
|
console.error(`[Flywheel] Failed to save entities to StateDb: ${e}`);
|
|
@@ -5505,14 +5649,14 @@ function isEntityIndexReady() {
|
|
|
5505
5649
|
return indexReady && entityIndex !== null;
|
|
5506
5650
|
}
|
|
5507
5651
|
function checkAndRefreshIfStale() {
|
|
5508
|
-
if (!
|
|
5652
|
+
if (!moduleStateDb5 || !indexReady) return;
|
|
5509
5653
|
try {
|
|
5510
|
-
const metadata = getStateDbMetadata(
|
|
5654
|
+
const metadata = getStateDbMetadata(moduleStateDb5);
|
|
5511
5655
|
if (!metadata.entitiesBuiltAt) return;
|
|
5512
5656
|
const dbBuiltAt = new Date(metadata.entitiesBuiltAt).getTime();
|
|
5513
5657
|
if (dbBuiltAt > lastLoadedAt) {
|
|
5514
5658
|
console.error("[Flywheel] Entity index stale, reloading from StateDb...");
|
|
5515
|
-
const dbIndex = getEntityIndexFromDb(
|
|
5659
|
+
const dbIndex = getEntityIndexFromDb(moduleStateDb5);
|
|
5516
5660
|
if (dbIndex._metadata.total_entities > 0) {
|
|
5517
5661
|
entityIndex = dbIndex;
|
|
5518
5662
|
lastLoadedAt = Date.now();
|
|
@@ -5554,11 +5698,11 @@ function processWikilinks(content, notePath, existingContent) {
|
|
|
5554
5698
|
}
|
|
5555
5699
|
let entities = getAllEntities(entityIndex);
|
|
5556
5700
|
console.error(`[Flywheel:DEBUG] Processing wikilinks with ${entities.length} entities`);
|
|
5557
|
-
if (
|
|
5701
|
+
if (moduleStateDb5) {
|
|
5558
5702
|
const folder = notePath ? notePath.split("/")[0] : void 0;
|
|
5559
5703
|
entities = entities.filter((e) => {
|
|
5560
5704
|
const name = getEntityName2(e);
|
|
5561
|
-
return !isSuppressed(
|
|
5705
|
+
return !isSuppressed(moduleStateDb5, name, folder);
|
|
5562
5706
|
});
|
|
5563
5707
|
}
|
|
5564
5708
|
const sortedEntities = sortEntitiesByPriority(entities, notePath);
|
|
@@ -5638,8 +5782,8 @@ function maybeApplyWikilinks(content, skipWikilinks, notePath, existingContent)
|
|
|
5638
5782
|
checkAndRefreshIfStale();
|
|
5639
5783
|
const result = processWikilinks(content, notePath, existingContent);
|
|
5640
5784
|
if (result.linksAdded > 0) {
|
|
5641
|
-
if (
|
|
5642
|
-
trackWikilinkApplications(
|
|
5785
|
+
if (moduleStateDb5 && notePath) {
|
|
5786
|
+
trackWikilinkApplications(moduleStateDb5, notePath, result.linkedEntities);
|
|
5643
5787
|
}
|
|
5644
5788
|
const implicitCount = result.implicitEntities?.length ?? 0;
|
|
5645
5789
|
const implicitInfo = implicitCount > 0 ? ` + ${implicitCount} implicit: ${result.implicitEntities.join(", ")}` : "";
|
|
@@ -5993,6 +6137,11 @@ function scoreEntity(entity, contentTokens, contentStems, config) {
|
|
|
5993
6137
|
}
|
|
5994
6138
|
return score;
|
|
5995
6139
|
}
|
|
6140
|
+
function getEdgeWeightBoostScore(entityName, map) {
|
|
6141
|
+
const avgWeight = map.get(entityName.toLowerCase());
|
|
6142
|
+
if (!avgWeight) return 0;
|
|
6143
|
+
return Math.min((avgWeight - 1) * 2, 4);
|
|
6144
|
+
}
|
|
5996
6145
|
async function suggestRelatedLinks(content, options = {}) {
|
|
5997
6146
|
const {
|
|
5998
6147
|
maxSuggestions = 3,
|
|
@@ -6036,7 +6185,8 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6036
6185
|
}
|
|
6037
6186
|
const linkedEntities = excludeLinked ? extractLinkedEntities(content) : /* @__PURE__ */ new Set();
|
|
6038
6187
|
const noteFolder = notePath ? notePath.split("/")[0] : void 0;
|
|
6039
|
-
const feedbackBoosts =
|
|
6188
|
+
const feedbackBoosts = moduleStateDb5 ? getAllFeedbackBoosts(moduleStateDb5, noteFolder) : /* @__PURE__ */ new Map();
|
|
6189
|
+
const edgeWeightMap = moduleStateDb5 ? getEntityEdgeWeightMap(moduleStateDb5) : /* @__PURE__ */ new Map();
|
|
6040
6190
|
const scoredEntities = [];
|
|
6041
6191
|
const directlyMatchedEntities = /* @__PURE__ */ new Set();
|
|
6042
6192
|
const entitiesWithContentMatch = /* @__PURE__ */ new Set();
|
|
@@ -6069,6 +6219,8 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6069
6219
|
score += layerHubBoost;
|
|
6070
6220
|
const layerFeedbackAdj = disabled.has("feedback") ? 0 : feedbackBoosts.get(entityName) ?? 0;
|
|
6071
6221
|
score += layerFeedbackAdj;
|
|
6222
|
+
const layerEdgeWeightBoost = disabled.has("edge_weight") ? 0 : getEdgeWeightBoostScore(entityName, edgeWeightMap);
|
|
6223
|
+
score += layerEdgeWeightBoost;
|
|
6072
6224
|
if (score > 0) {
|
|
6073
6225
|
directlyMatchedEntities.add(entityName);
|
|
6074
6226
|
}
|
|
@@ -6086,7 +6238,8 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6086
6238
|
recencyBoost: layerRecencyBoost,
|
|
6087
6239
|
crossFolderBoost: layerCrossFolderBoost,
|
|
6088
6240
|
hubBoost: layerHubBoost,
|
|
6089
|
-
feedbackAdjustment: layerFeedbackAdj
|
|
6241
|
+
feedbackAdjustment: layerFeedbackAdj,
|
|
6242
|
+
edgeWeightBoost: layerEdgeWeightBoost
|
|
6090
6243
|
}
|
|
6091
6244
|
});
|
|
6092
6245
|
}
|
|
@@ -6119,7 +6272,8 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6119
6272
|
const crossFolderBoost = disabled.has("cross_folder") ? 0 : notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
6120
6273
|
const hubBoost = disabled.has("hub_boost") ? 0 : getHubBoost(entity);
|
|
6121
6274
|
const feedbackAdj = disabled.has("feedback") ? 0 : feedbackBoosts.get(entityName) ?? 0;
|
|
6122
|
-
const
|
|
6275
|
+
const edgeWeightBoost = disabled.has("edge_weight") ? 0 : getEdgeWeightBoostScore(entityName, edgeWeightMap);
|
|
6276
|
+
const totalBoost = boost + typeBoost + contextBoost + recencyBoostVal + crossFolderBoost + hubBoost + feedbackAdj + edgeWeightBoost;
|
|
6123
6277
|
if (totalBoost >= adaptiveMinScore) {
|
|
6124
6278
|
scoredEntities.push({
|
|
6125
6279
|
name: entityName,
|
|
@@ -6134,7 +6288,8 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6134
6288
|
recencyBoost: recencyBoostVal,
|
|
6135
6289
|
crossFolderBoost,
|
|
6136
6290
|
hubBoost,
|
|
6137
|
-
feedbackAdjustment: feedbackAdj
|
|
6291
|
+
feedbackAdjustment: feedbackAdj,
|
|
6292
|
+
edgeWeightBoost
|
|
6138
6293
|
}
|
|
6139
6294
|
});
|
|
6140
6295
|
}
|
|
@@ -6172,7 +6327,8 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6172
6327
|
const layerHubBoost = disabled.has("hub_boost") ? 0 : getHubBoost(entity);
|
|
6173
6328
|
const layerCrossFolderBoost = disabled.has("cross_folder") ? 0 : notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
6174
6329
|
const layerFeedbackAdj = disabled.has("feedback") ? 0 : feedbackBoosts.get(match.entityName) ?? 0;
|
|
6175
|
-
const
|
|
6330
|
+
const layerEdgeWeightBoost = disabled.has("edge_weight") ? 0 : getEdgeWeightBoostScore(match.entityName, edgeWeightMap);
|
|
6331
|
+
const totalScore = boost + layerTypeBoost + layerContextBoost + layerHubBoost + layerCrossFolderBoost + layerFeedbackAdj + layerEdgeWeightBoost;
|
|
6176
6332
|
if (totalScore >= adaptiveMinScore) {
|
|
6177
6333
|
scoredEntities.push({
|
|
6178
6334
|
name: match.entityName,
|
|
@@ -6188,7 +6344,8 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6188
6344
|
crossFolderBoost: layerCrossFolderBoost,
|
|
6189
6345
|
hubBoost: layerHubBoost,
|
|
6190
6346
|
feedbackAdjustment: layerFeedbackAdj,
|
|
6191
|
-
semanticBoost: boost
|
|
6347
|
+
semanticBoost: boost,
|
|
6348
|
+
edgeWeightBoost: layerEdgeWeightBoost
|
|
6192
6349
|
}
|
|
6193
6350
|
});
|
|
6194
6351
|
entitiesWithContentMatch.add(match.entityName);
|
|
@@ -6213,15 +6370,15 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6213
6370
|
}
|
|
6214
6371
|
return 0;
|
|
6215
6372
|
});
|
|
6216
|
-
if (
|
|
6373
|
+
if (moduleStateDb5 && notePath) {
|
|
6217
6374
|
try {
|
|
6218
6375
|
const now = Date.now();
|
|
6219
|
-
const insertStmt =
|
|
6376
|
+
const insertStmt = moduleStateDb5.db.prepare(`
|
|
6220
6377
|
INSERT OR IGNORE INTO suggestion_events
|
|
6221
6378
|
(timestamp, note_path, entity, total_score, breakdown_json, threshold, passed, strictness, applied, pipeline_event_id)
|
|
6222
6379
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL)
|
|
6223
6380
|
`);
|
|
6224
|
-
const persistTransaction =
|
|
6381
|
+
const persistTransaction = moduleStateDb5.db.transaction(() => {
|
|
6225
6382
|
for (const e of relevantEntities) {
|
|
6226
6383
|
insertStmt.run(
|
|
6227
6384
|
now,
|
|
@@ -6268,7 +6425,7 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6268
6425
|
suffix
|
|
6269
6426
|
};
|
|
6270
6427
|
if (detail) {
|
|
6271
|
-
const feedbackStats =
|
|
6428
|
+
const feedbackStats = moduleStateDb5 ? getEntityStats(moduleStateDb5) : [];
|
|
6272
6429
|
const feedbackMap = new Map(feedbackStats.map((s) => [s.entity, s]));
|
|
6273
6430
|
result.detailed = topEntries.map((e) => {
|
|
6274
6431
|
const fb = feedbackMap.get(e.name);
|
|
@@ -6287,9 +6444,9 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6287
6444
|
return result;
|
|
6288
6445
|
}
|
|
6289
6446
|
function detectAliasCollisions(noteName, aliases = []) {
|
|
6290
|
-
if (!
|
|
6447
|
+
if (!moduleStateDb5) return [];
|
|
6291
6448
|
const collisions = [];
|
|
6292
|
-
const nameAsAlias = getEntitiesByAlias(
|
|
6449
|
+
const nameAsAlias = getEntitiesByAlias(moduleStateDb5, noteName);
|
|
6293
6450
|
for (const entity of nameAsAlias) {
|
|
6294
6451
|
if (entity.name.toLowerCase() === noteName.toLowerCase()) continue;
|
|
6295
6452
|
collisions.push({
|
|
@@ -6303,7 +6460,7 @@ function detectAliasCollisions(noteName, aliases = []) {
|
|
|
6303
6460
|
});
|
|
6304
6461
|
}
|
|
6305
6462
|
for (const alias of aliases) {
|
|
6306
|
-
const existingByName = getEntityByName(
|
|
6463
|
+
const existingByName = getEntityByName(moduleStateDb5, alias);
|
|
6307
6464
|
if (existingByName && existingByName.name.toLowerCase() !== noteName.toLowerCase()) {
|
|
6308
6465
|
collisions.push({
|
|
6309
6466
|
term: alias,
|
|
@@ -6315,7 +6472,7 @@ function detectAliasCollisions(noteName, aliases = []) {
|
|
|
6315
6472
|
}
|
|
6316
6473
|
});
|
|
6317
6474
|
}
|
|
6318
|
-
const existingByAlias = getEntitiesByAlias(
|
|
6475
|
+
const existingByAlias = getEntitiesByAlias(moduleStateDb5, alias);
|
|
6319
6476
|
for (const entity of existingByAlias) {
|
|
6320
6477
|
if (entity.name.toLowerCase() === noteName.toLowerCase()) continue;
|
|
6321
6478
|
if (existingByName && existingByName.name.toLowerCase() === entity.name.toLowerCase()) continue;
|
|
@@ -6339,8 +6496,8 @@ function suggestAliases(noteName, existingAliases = [], category) {
|
|
|
6339
6496
|
function isSafe(alias) {
|
|
6340
6497
|
if (existingLower.has(alias.toLowerCase())) return false;
|
|
6341
6498
|
if (alias.toLowerCase() === noteName.toLowerCase()) return false;
|
|
6342
|
-
if (!
|
|
6343
|
-
const existing = getEntityByName(
|
|
6499
|
+
if (!moduleStateDb5) return true;
|
|
6500
|
+
const existing = getEntityByName(moduleStateDb5, alias);
|
|
6344
6501
|
return !existing;
|
|
6345
6502
|
}
|
|
6346
6503
|
const inferredCategory = category || inferCategoryFromName(noteName);
|
|
@@ -6382,8 +6539,8 @@ function inferCategoryFromName(name) {
|
|
|
6382
6539
|
}
|
|
6383
6540
|
async function checkPreflightSimilarity(noteName) {
|
|
6384
6541
|
const result = { similarEntities: [] };
|
|
6385
|
-
if (!
|
|
6386
|
-
const exact = getEntityByName(
|
|
6542
|
+
if (!moduleStateDb5) return result;
|
|
6543
|
+
const exact = getEntityByName(moduleStateDb5, noteName);
|
|
6387
6544
|
if (exact) {
|
|
6388
6545
|
result.existingEntity = {
|
|
6389
6546
|
name: exact.name,
|
|
@@ -6393,7 +6550,7 @@ async function checkPreflightSimilarity(noteName) {
|
|
|
6393
6550
|
}
|
|
6394
6551
|
const ftsNames = /* @__PURE__ */ new Set();
|
|
6395
6552
|
try {
|
|
6396
|
-
const searchResults = searchEntitiesDb(
|
|
6553
|
+
const searchResults = searchEntitiesDb(moduleStateDb5, noteName, 5);
|
|
6397
6554
|
for (const sr of searchResults) {
|
|
6398
6555
|
if (sr.name.toLowerCase() === noteName.toLowerCase()) continue;
|
|
6399
6556
|
ftsNames.add(sr.name.toLowerCase());
|
|
@@ -6414,7 +6571,7 @@ async function checkPreflightSimilarity(noteName) {
|
|
|
6414
6571
|
if (match.similarity < 0.85) continue;
|
|
6415
6572
|
if (match.entityName.toLowerCase() === noteName.toLowerCase()) continue;
|
|
6416
6573
|
if (ftsNames.has(match.entityName.toLowerCase())) continue;
|
|
6417
|
-
const entity = getEntityByName(
|
|
6574
|
+
const entity = getEntityByName(moduleStateDb5, match.entityName);
|
|
6418
6575
|
if (entity) {
|
|
6419
6576
|
result.similarEntities.push({
|
|
6420
6577
|
name: entity.name,
|
|
@@ -9202,123 +9359,6 @@ function suggestEntityAliases(stateDb2, folder) {
|
|
|
9202
9359
|
return suggestions;
|
|
9203
9360
|
}
|
|
9204
9361
|
|
|
9205
|
-
// src/core/write/edgeWeights.ts
|
|
9206
|
-
var moduleStateDb5 = null;
|
|
9207
|
-
function setEdgeWeightStateDb(stateDb2) {
|
|
9208
|
-
moduleStateDb5 = stateDb2;
|
|
9209
|
-
}
|
|
9210
|
-
function buildPathToTargetsMap(stateDb2) {
|
|
9211
|
-
const map = /* @__PURE__ */ new Map();
|
|
9212
|
-
const rows = stateDb2.db.prepare(
|
|
9213
|
-
"SELECT path, name_lower, aliases_json FROM entities"
|
|
9214
|
-
).all();
|
|
9215
|
-
for (const row of rows) {
|
|
9216
|
-
const targets = /* @__PURE__ */ new Set();
|
|
9217
|
-
targets.add(row.name_lower);
|
|
9218
|
-
if (row.aliases_json) {
|
|
9219
|
-
try {
|
|
9220
|
-
const aliases = JSON.parse(row.aliases_json);
|
|
9221
|
-
for (const alias of aliases) {
|
|
9222
|
-
targets.add(alias.toLowerCase());
|
|
9223
|
-
}
|
|
9224
|
-
} catch {
|
|
9225
|
-
}
|
|
9226
|
-
}
|
|
9227
|
-
map.set(row.path, targets);
|
|
9228
|
-
}
|
|
9229
|
-
return map;
|
|
9230
|
-
}
|
|
9231
|
-
function pathToFallbackTarget(filePath) {
|
|
9232
|
-
return filePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? filePath.toLowerCase();
|
|
9233
|
-
}
|
|
9234
|
-
function recomputeEdgeWeights(stateDb2) {
|
|
9235
|
-
const start = Date.now();
|
|
9236
|
-
const edges = stateDb2.db.prepare(
|
|
9237
|
-
"SELECT note_path, target FROM note_links"
|
|
9238
|
-
).all();
|
|
9239
|
-
if (edges.length === 0) {
|
|
9240
|
-
return { edges_updated: 0, duration_ms: Date.now() - start };
|
|
9241
|
-
}
|
|
9242
|
-
const survivalMap = /* @__PURE__ */ new Map();
|
|
9243
|
-
const historyRows = stateDb2.db.prepare(
|
|
9244
|
-
"SELECT note_path, target, edits_survived FROM note_link_history"
|
|
9245
|
-
).all();
|
|
9246
|
-
for (const row of historyRows) {
|
|
9247
|
-
survivalMap.set(`${row.note_path}\0${row.target}`, row.edits_survived);
|
|
9248
|
-
}
|
|
9249
|
-
const pathToTargets = buildPathToTargetsMap(stateDb2);
|
|
9250
|
-
const targetToPaths = /* @__PURE__ */ new Map();
|
|
9251
|
-
for (const [entityPath, targets] of pathToTargets) {
|
|
9252
|
-
for (const target of targets) {
|
|
9253
|
-
let paths = targetToPaths.get(target);
|
|
9254
|
-
if (!paths) {
|
|
9255
|
-
paths = /* @__PURE__ */ new Set();
|
|
9256
|
-
targetToPaths.set(target, paths);
|
|
9257
|
-
}
|
|
9258
|
-
paths.add(entityPath);
|
|
9259
|
-
}
|
|
9260
|
-
}
|
|
9261
|
-
const sessionRows = stateDb2.db.prepare(
|
|
9262
|
-
`SELECT session_id, note_paths FROM tool_invocations
|
|
9263
|
-
WHERE note_paths IS NOT NULL AND note_paths != '[]'`
|
|
9264
|
-
).all();
|
|
9265
|
-
const sessionPaths = /* @__PURE__ */ new Map();
|
|
9266
|
-
for (const row of sessionRows) {
|
|
9267
|
-
try {
|
|
9268
|
-
const paths = JSON.parse(row.note_paths);
|
|
9269
|
-
if (!Array.isArray(paths) || paths.length === 0) continue;
|
|
9270
|
-
let existing = sessionPaths.get(row.session_id);
|
|
9271
|
-
if (!existing) {
|
|
9272
|
-
existing = /* @__PURE__ */ new Set();
|
|
9273
|
-
sessionPaths.set(row.session_id, existing);
|
|
9274
|
-
}
|
|
9275
|
-
for (const p of paths) {
|
|
9276
|
-
existing.add(p);
|
|
9277
|
-
}
|
|
9278
|
-
} catch {
|
|
9279
|
-
}
|
|
9280
|
-
}
|
|
9281
|
-
const coSessionCount = /* @__PURE__ */ new Map();
|
|
9282
|
-
const sourceActivityCount = /* @__PURE__ */ new Map();
|
|
9283
|
-
for (const [, paths] of sessionPaths) {
|
|
9284
|
-
const sessionTargets = /* @__PURE__ */ new Set();
|
|
9285
|
-
for (const p of paths) {
|
|
9286
|
-
const targets = pathToTargets.get(p);
|
|
9287
|
-
if (targets) {
|
|
9288
|
-
for (const t of targets) sessionTargets.add(t);
|
|
9289
|
-
} else {
|
|
9290
|
-
sessionTargets.add(pathToFallbackTarget(p));
|
|
9291
|
-
}
|
|
9292
|
-
}
|
|
9293
|
-
for (const edge of edges) {
|
|
9294
|
-
if (paths.has(edge.note_path)) {
|
|
9295
|
-
const srcKey = edge.note_path;
|
|
9296
|
-
sourceActivityCount.set(srcKey, (sourceActivityCount.get(srcKey) ?? 0) + 1);
|
|
9297
|
-
if (sessionTargets.has(edge.target)) {
|
|
9298
|
-
const edgeKey = `${edge.note_path}\0${edge.target}`;
|
|
9299
|
-
coSessionCount.set(edgeKey, (coSessionCount.get(edgeKey) ?? 0) + 1);
|
|
9300
|
-
}
|
|
9301
|
-
}
|
|
9302
|
-
}
|
|
9303
|
-
}
|
|
9304
|
-
const now = Date.now();
|
|
9305
|
-
const update = stateDb2.db.prepare(
|
|
9306
|
-
"UPDATE note_links SET weight = ?, weight_updated_at = ? WHERE note_path = ? AND target = ?"
|
|
9307
|
-
);
|
|
9308
|
-
const tx = stateDb2.db.transaction(() => {
|
|
9309
|
-
for (const edge of edges) {
|
|
9310
|
-
const edgeKey = `${edge.note_path}\0${edge.target}`;
|
|
9311
|
-
const editsSurvived = survivalMap.get(edgeKey) ?? 0;
|
|
9312
|
-
const coSessions = coSessionCount.get(edgeKey) ?? 0;
|
|
9313
|
-
const sourceAccess = sourceActivityCount.get(edge.note_path) ?? 0;
|
|
9314
|
-
const weight = 1 + editsSurvived * 0.5 + Math.min(coSessions * 0.5, 3) + Math.min(sourceAccess * 0.2, 2);
|
|
9315
|
-
update.run(Math.round(weight * 1e3) / 1e3, now, edge.note_path, edge.target);
|
|
9316
|
-
}
|
|
9317
|
-
});
|
|
9318
|
-
tx();
|
|
9319
|
-
return { edges_updated: edges.length, duration_ms: Date.now() - start };
|
|
9320
|
-
}
|
|
9321
|
-
|
|
9322
9362
|
// src/tools/read/system.ts
|
|
9323
9363
|
function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
|
|
9324
9364
|
const RefreshIndexOutputSchema = {
|
|
@@ -13965,6 +14005,108 @@ ${trimmedSource}`;
|
|
|
13965
14005
|
}
|
|
13966
14006
|
}
|
|
13967
14007
|
);
|
|
14008
|
+
server2.tool(
|
|
14009
|
+
"absorb_as_alias",
|
|
14010
|
+
"Absorb an entity name as an alias of a target note: adds alias to target frontmatter and rewrites all [[source]] links to [[target|source]]. Lighter than merge_entities \u2014 no source note required, no content append, no deletion.",
|
|
14011
|
+
{
|
|
14012
|
+
source_name: z16.string().describe('The entity name to absorb (e.g. "Foo")'),
|
|
14013
|
+
target_path: z16.string().describe('Vault-relative path of the target entity note (e.g. "entities/Bar.md")')
|
|
14014
|
+
},
|
|
14015
|
+
async ({ source_name, target_path }) => {
|
|
14016
|
+
try {
|
|
14017
|
+
if (!validatePath(vaultPath2, target_path)) {
|
|
14018
|
+
const result2 = {
|
|
14019
|
+
success: false,
|
|
14020
|
+
message: "Invalid target path: path traversal not allowed",
|
|
14021
|
+
path: target_path
|
|
14022
|
+
};
|
|
14023
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
14024
|
+
}
|
|
14025
|
+
let targetContent;
|
|
14026
|
+
let targetFrontmatter;
|
|
14027
|
+
try {
|
|
14028
|
+
const target = await readVaultFile(vaultPath2, target_path);
|
|
14029
|
+
targetContent = target.content;
|
|
14030
|
+
targetFrontmatter = target.frontmatter;
|
|
14031
|
+
} catch {
|
|
14032
|
+
const result2 = {
|
|
14033
|
+
success: false,
|
|
14034
|
+
message: `Target file not found: ${target_path}`,
|
|
14035
|
+
path: target_path
|
|
14036
|
+
};
|
|
14037
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
14038
|
+
}
|
|
14039
|
+
const targetTitle = getTitleFromPath(target_path);
|
|
14040
|
+
const existingAliases = extractAliases2(targetFrontmatter);
|
|
14041
|
+
const deduped = new Set(existingAliases);
|
|
14042
|
+
if (source_name.toLowerCase() !== targetTitle.toLowerCase()) {
|
|
14043
|
+
deduped.add(source_name);
|
|
14044
|
+
}
|
|
14045
|
+
targetFrontmatter.aliases = Array.from(deduped);
|
|
14046
|
+
await writeVaultFile(vaultPath2, target_path, targetContent, targetFrontmatter);
|
|
14047
|
+
const backlinks = await findBacklinks(vaultPath2, source_name, []);
|
|
14048
|
+
let totalBacklinksUpdated = 0;
|
|
14049
|
+
const modifiedFiles = [];
|
|
14050
|
+
for (const backlink of backlinks) {
|
|
14051
|
+
if (backlink.path === target_path) continue;
|
|
14052
|
+
let fileData;
|
|
14053
|
+
try {
|
|
14054
|
+
fileData = await readVaultFile(vaultPath2, backlink.path);
|
|
14055
|
+
} catch {
|
|
14056
|
+
continue;
|
|
14057
|
+
}
|
|
14058
|
+
let content = fileData.content;
|
|
14059
|
+
let linksUpdated = 0;
|
|
14060
|
+
const pattern = new RegExp(
|
|
14061
|
+
`\\[\\[${escapeRegex(source_name)}(\\|[^\\]]+)?\\]\\]`,
|
|
14062
|
+
"gi"
|
|
14063
|
+
);
|
|
14064
|
+
content = content.replace(pattern, (_match, displayPart) => {
|
|
14065
|
+
linksUpdated++;
|
|
14066
|
+
if (displayPart) {
|
|
14067
|
+
return `[[${targetTitle}${displayPart}]]`;
|
|
14068
|
+
}
|
|
14069
|
+
if (source_name.toLowerCase() === targetTitle.toLowerCase()) {
|
|
14070
|
+
return `[[${targetTitle}]]`;
|
|
14071
|
+
}
|
|
14072
|
+
return `[[${targetTitle}|${source_name}]]`;
|
|
14073
|
+
});
|
|
14074
|
+
if (linksUpdated > 0) {
|
|
14075
|
+
await writeVaultFile(vaultPath2, backlink.path, content, fileData.frontmatter);
|
|
14076
|
+
totalBacklinksUpdated += linksUpdated;
|
|
14077
|
+
modifiedFiles.push(backlink.path);
|
|
14078
|
+
}
|
|
14079
|
+
}
|
|
14080
|
+
initializeEntityIndex(vaultPath2).catch((err) => {
|
|
14081
|
+
console.error(`[Flywheel] Entity cache rebuild failed: ${err}`);
|
|
14082
|
+
});
|
|
14083
|
+
const aliasAdded = source_name.toLowerCase() !== targetTitle.toLowerCase();
|
|
14084
|
+
const previewLines = [
|
|
14085
|
+
`Absorbed: "${source_name}" \u2192 "${targetTitle}"`,
|
|
14086
|
+
`Alias added: ${aliasAdded ? source_name : "no (matches target title)"}`,
|
|
14087
|
+
`Backlinks updated: ${totalBacklinksUpdated}`
|
|
14088
|
+
];
|
|
14089
|
+
if (modifiedFiles.length > 0) {
|
|
14090
|
+
previewLines.push(`Files modified: ${modifiedFiles.join(", ")}`);
|
|
14091
|
+
}
|
|
14092
|
+
const result = {
|
|
14093
|
+
success: true,
|
|
14094
|
+
message: `Absorbed "${source_name}" as alias of "${targetTitle}"`,
|
|
14095
|
+
path: target_path,
|
|
14096
|
+
preview: previewLines.join("\n"),
|
|
14097
|
+
backlinks_updated: totalBacklinksUpdated
|
|
14098
|
+
};
|
|
14099
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
14100
|
+
} catch (error) {
|
|
14101
|
+
const result = {
|
|
14102
|
+
success: false,
|
|
14103
|
+
message: `Failed to absorb as alias: ${error instanceof Error ? error.message : String(error)}`,
|
|
14104
|
+
path: target_path
|
|
14105
|
+
};
|
|
14106
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
14107
|
+
}
|
|
14108
|
+
}
|
|
14109
|
+
);
|
|
13968
14110
|
}
|
|
13969
14111
|
|
|
13970
14112
|
// src/tools/write/system.ts
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
|
|
3
|
+
"version": "2.0.43",
|
|
4
|
+
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -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.43",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|