@velvetmonkey/flywheel-memory 2.4.0 → 2.4.2
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 +154 -14
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2069,14 +2069,15 @@ function getAllSuppressionPenalties(stateDb2, now) {
|
|
|
2069
2069
|
}
|
|
2070
2070
|
return penalties;
|
|
2071
2071
|
}
|
|
2072
|
-
function trackWikilinkApplications(stateDb2, notePath, entities) {
|
|
2072
|
+
function trackWikilinkApplications(stateDb2, notePath, entities, source = "tool") {
|
|
2073
2073
|
const upsert = stateDb2.db.prepare(`
|
|
2074
|
-
INSERT INTO wikilink_applications (entity, note_path, matched_term, applied_at, status)
|
|
2075
|
-
VALUES (?, ?, ?, datetime('now'), 'applied')
|
|
2074
|
+
INSERT INTO wikilink_applications (entity, note_path, matched_term, applied_at, status, source)
|
|
2075
|
+
VALUES (?, ?, ?, datetime('now'), 'applied', ?)
|
|
2076
2076
|
ON CONFLICT(entity, note_path) DO UPDATE SET
|
|
2077
2077
|
matched_term = COALESCE(?, matched_term),
|
|
2078
2078
|
applied_at = datetime('now'),
|
|
2079
|
-
status = 'applied'
|
|
2079
|
+
status = 'applied',
|
|
2080
|
+
source = ?
|
|
2080
2081
|
`);
|
|
2081
2082
|
const lookupCanonical = stateDb2.db.prepare(
|
|
2082
2083
|
`SELECT name FROM entities WHERE LOWER(name) = LOWER(?) LIMIT 1`
|
|
@@ -2087,7 +2088,7 @@ function trackWikilinkApplications(stateDb2, notePath, entities) {
|
|
|
2087
2088
|
const matchedTerm = typeof item === "string" ? null : item.matchedTerm ?? null;
|
|
2088
2089
|
const row = lookupCanonical.get(entityName);
|
|
2089
2090
|
const canonicalName = row?.name ?? entityName;
|
|
2090
|
-
upsert.run(canonicalName, notePath, matchedTerm, matchedTerm);
|
|
2091
|
+
upsert.run(canonicalName, notePath, matchedTerm, source, matchedTerm, source);
|
|
2091
2092
|
}
|
|
2092
2093
|
});
|
|
2093
2094
|
transaction();
|
|
@@ -4718,7 +4719,7 @@ async function applyProactiveSuggestions(filePath, vaultPath2, suggestions, conf
|
|
|
4718
4719
|
return { applied: [], skipped: candidates.map((c) => c.entity) };
|
|
4719
4720
|
}
|
|
4720
4721
|
if (stateDb2) {
|
|
4721
|
-
trackWikilinkApplications(stateDb2, filePath, result.linkedEntities);
|
|
4722
|
+
trackWikilinkApplications(stateDb2, filePath, result.linkedEntities, "proactive");
|
|
4722
4723
|
try {
|
|
4723
4724
|
const markApplied = stateDb2.db.prepare(
|
|
4724
4725
|
`UPDATE suggestion_events SET applied = 1
|
|
@@ -5001,7 +5002,7 @@ async function drainProactiveQueue(stateDb2, vaultPath2, config, applyFn) {
|
|
|
5001
5002
|
todayMidnight.setHours(0, 0, 0, 0);
|
|
5002
5003
|
const todayStr = todayMidnight.toISOString().slice(0, 10);
|
|
5003
5004
|
const countTodayApplied = stateDb2.db.prepare(
|
|
5004
|
-
`SELECT COUNT(*) as cnt FROM wikilink_applications WHERE note_path = ? AND applied_at >=
|
|
5005
|
+
`SELECT COUNT(*) as cnt FROM wikilink_applications WHERE note_path = ? AND applied_at >= ? AND source = 'proactive'`
|
|
5005
5006
|
);
|
|
5006
5007
|
for (const [filePath, suggestions] of byFile) {
|
|
5007
5008
|
const fullPath = path12.join(vaultPath2, filePath);
|
|
@@ -10013,7 +10014,7 @@ var PipelineRunner = class {
|
|
|
10013
10014
|
}
|
|
10014
10015
|
}
|
|
10015
10016
|
if (newlyTracked.length > 0) {
|
|
10016
|
-
trackWikilinkApplications(p.sd, diff.file, newlyTracked);
|
|
10017
|
+
trackWikilinkApplications(p.sd, diff.file, newlyTracked, "manual_detected");
|
|
10017
10018
|
}
|
|
10018
10019
|
}
|
|
10019
10020
|
}
|
|
@@ -15402,6 +15403,55 @@ function getActivitySummary(index, days) {
|
|
|
15402
15403
|
import { SCHEMA_VERSION } from "@velvetmonkey/vault-core";
|
|
15403
15404
|
init_embeddings();
|
|
15404
15405
|
init_serverLog();
|
|
15406
|
+
|
|
15407
|
+
// src/core/shared/proactiveLinkingStats.ts
|
|
15408
|
+
function toSqliteTimestamp(date) {
|
|
15409
|
+
return date.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
|
|
15410
|
+
}
|
|
15411
|
+
function getProactiveLinkingSummary(stateDb2, daysBack = 1) {
|
|
15412
|
+
const now = /* @__PURE__ */ new Date();
|
|
15413
|
+
const since = new Date(now.getTime() - daysBack * 24 * 60 * 60 * 1e3);
|
|
15414
|
+
const sinceStr = toSqliteTimestamp(since);
|
|
15415
|
+
const untilStr = toSqliteTimestamp(now);
|
|
15416
|
+
const survived = stateDb2.db.prepare(
|
|
15417
|
+
`SELECT COUNT(*) as cnt FROM wikilink_applications
|
|
15418
|
+
WHERE source = 'proactive' AND applied_at >= ? AND status = 'applied'`
|
|
15419
|
+
).get(sinceStr);
|
|
15420
|
+
const removed = stateDb2.db.prepare(
|
|
15421
|
+
`SELECT COUNT(*) as cnt FROM wikilink_applications
|
|
15422
|
+
WHERE source = 'proactive' AND applied_at >= ? AND status = 'removed'`
|
|
15423
|
+
).get(sinceStr);
|
|
15424
|
+
const files = stateDb2.db.prepare(
|
|
15425
|
+
`SELECT COUNT(DISTINCT note_path) as cnt FROM wikilink_applications
|
|
15426
|
+
WHERE source = 'proactive' AND applied_at >= ?`
|
|
15427
|
+
).get(sinceStr);
|
|
15428
|
+
const recent = stateDb2.db.prepare(
|
|
15429
|
+
`SELECT entity, note_path, applied_at, status FROM wikilink_applications
|
|
15430
|
+
WHERE source = 'proactive' AND applied_at >= ?
|
|
15431
|
+
ORDER BY applied_at DESC LIMIT 10`
|
|
15432
|
+
).all(sinceStr);
|
|
15433
|
+
const totalApplied = survived.cnt + removed.cnt;
|
|
15434
|
+
const survivalRate = totalApplied > 0 ? survived.cnt / totalApplied : null;
|
|
15435
|
+
return {
|
|
15436
|
+
window: { kind: "rolling_24h", since: sinceStr, until: untilStr },
|
|
15437
|
+
total_applied: totalApplied,
|
|
15438
|
+
survived: survived.cnt,
|
|
15439
|
+
removed: removed.cnt,
|
|
15440
|
+
files_touched: files.cnt,
|
|
15441
|
+
survival_rate: survivalRate,
|
|
15442
|
+
recent
|
|
15443
|
+
};
|
|
15444
|
+
}
|
|
15445
|
+
function getProactiveLinkingOneLiner(stateDb2, daysBack = 1) {
|
|
15446
|
+
const summary = getProactiveLinkingSummary(stateDb2, daysBack);
|
|
15447
|
+
if (summary.total_applied === 0) return null;
|
|
15448
|
+
const linkWord = summary.total_applied === 1 ? "link" : "links";
|
|
15449
|
+
const noteWord = summary.files_touched === 1 ? "note" : "notes";
|
|
15450
|
+
const rate = summary.survival_rate !== null ? `${Math.round(summary.survival_rate * 100)}%` : "n/a";
|
|
15451
|
+
return `${summary.total_applied} ${linkWord} applied across ${summary.files_touched} ${noteWord} (${summary.survived} survived, ${rate} rate)`;
|
|
15452
|
+
}
|
|
15453
|
+
|
|
15454
|
+
// src/tools/read/health.ts
|
|
15405
15455
|
init_wikilinkFeedback();
|
|
15406
15456
|
init_embeddings();
|
|
15407
15457
|
var STALE_THRESHOLD_SECONDS = 300;
|
|
@@ -15527,6 +15577,15 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
15527
15577
|
unlinked_mentions: z5.number()
|
|
15528
15578
|
})).describe("Entities with the most unlinked plain-text mentions")
|
|
15529
15579
|
}).optional().describe("Background sweep results (graph hygiene metrics, updated every 5 min)"),
|
|
15580
|
+
proactive_linking: z5.object({
|
|
15581
|
+
enabled: z5.boolean().describe("Whether proactive linking is enabled in config"),
|
|
15582
|
+
queue_pending: z5.number().describe("Number of queued proactive suggestions awaiting drain"),
|
|
15583
|
+
summary: z5.string().nullable().describe('One-liner: "12 links applied across 8 notes (11 survived, 92% rate)"'),
|
|
15584
|
+
total_applied_24h: z5.number().describe("Total proactive applications in last 24h"),
|
|
15585
|
+
survived_24h: z5.number().describe("Proactive links still present"),
|
|
15586
|
+
removed_24h: z5.number().describe("Proactive links removed by user"),
|
|
15587
|
+
files_24h: z5.number().describe("Distinct files touched by proactive linking")
|
|
15588
|
+
}).optional().describe("Proactive linking observability (full mode only)"),
|
|
15530
15589
|
recommendations: z5.array(z5.string()).describe("Suggested actions if any issues detected")
|
|
15531
15590
|
};
|
|
15532
15591
|
server2.registerTool(
|
|
@@ -15779,6 +15838,24 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
15779
15838
|
dead_link_count: isFull ? deadLinkCount : void 0,
|
|
15780
15839
|
top_dead_link_targets: isFull ? topDeadLinkTargets : void 0,
|
|
15781
15840
|
sweep: isFull ? getSweepResults() ?? void 0 : void 0,
|
|
15841
|
+
proactive_linking: isFull && stateDb2 ? (() => {
|
|
15842
|
+
const config = getConfig2();
|
|
15843
|
+
const enabled = config.proactive_linking !== false;
|
|
15844
|
+
const queuePending = stateDb2.db.prepare(
|
|
15845
|
+
`SELECT COUNT(*) as cnt FROM proactive_queue WHERE status = 'pending'`
|
|
15846
|
+
).get();
|
|
15847
|
+
const summary = getProactiveLinkingSummary(stateDb2, 1);
|
|
15848
|
+
const oneLiner = getProactiveLinkingOneLiner(stateDb2, 1);
|
|
15849
|
+
return {
|
|
15850
|
+
enabled,
|
|
15851
|
+
queue_pending: queuePending.cnt,
|
|
15852
|
+
summary: oneLiner,
|
|
15853
|
+
total_applied_24h: summary.total_applied,
|
|
15854
|
+
survived_24h: summary.survived,
|
|
15855
|
+
removed_24h: summary.removed,
|
|
15856
|
+
files_24h: summary.files_touched
|
|
15857
|
+
};
|
|
15858
|
+
})() : void 0,
|
|
15782
15859
|
recommendations
|
|
15783
15860
|
};
|
|
15784
15861
|
return {
|
|
@@ -16085,10 +16162,17 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
16085
16162
|
checks.push({ name: "embedding_coverage", status: "ok", detail: `${embCount}/${noteCount} notes embedded (${coverage}%), model: ${getActiveModelId() || "default"}` });
|
|
16086
16163
|
}
|
|
16087
16164
|
const entityEmbCount = getEntityEmbeddingsCount();
|
|
16088
|
-
const entityCount =
|
|
16165
|
+
const entityCount = (() => {
|
|
16166
|
+
if (!stateDb2) return 0;
|
|
16167
|
+
try {
|
|
16168
|
+
return stateDb2.db.prepare("SELECT COUNT(*) as cnt FROM entities").get()?.cnt ?? 0;
|
|
16169
|
+
} catch {
|
|
16170
|
+
return 0;
|
|
16171
|
+
}
|
|
16172
|
+
})();
|
|
16089
16173
|
if (entityCount > 0) {
|
|
16090
16174
|
const entityCoverage = Math.round(entityEmbCount / entityCount * 100);
|
|
16091
|
-
checks.push({ name: "entity_embedding_coverage", status: entityCoverage < 50 ? "warning" : "ok", detail: `${entityEmbCount}/${entityCount} entities embedded (${entityCoverage}%)` });
|
|
16175
|
+
checks.push({ name: "entity_embedding_coverage", status: entityCoverage < 50 ? "warning" : "ok", detail: `${entityEmbCount}/${entityCount} canonical entities embedded (${entityCoverage}%)` });
|
|
16092
16176
|
}
|
|
16093
16177
|
} else if (!embReady) {
|
|
16094
16178
|
checks.push({ name: "embedding_coverage", status: "warning", detail: "Semantic embeddings not built", fix: "Run init_semantic to enable hybrid search" });
|
|
@@ -24021,6 +24105,24 @@ function buildVaultPulseSection(stateDb2) {
|
|
|
24021
24105
|
estimated_tokens: estimateTokens2(content)
|
|
24022
24106
|
};
|
|
24023
24107
|
}
|
|
24108
|
+
function buildProactiveLinkingSection(stateDb2) {
|
|
24109
|
+
const summary = getProactiveLinkingSummary(stateDb2, 1);
|
|
24110
|
+
if (summary.total_applied === 0) return null;
|
|
24111
|
+
const content = {
|
|
24112
|
+
summary: `${summary.total_applied} ${summary.total_applied === 1 ? "link" : "links"} applied across ${summary.files_touched} ${summary.files_touched === 1 ? "note" : "notes"} (${summary.survived} survived, ${summary.survival_rate !== null ? Math.round(summary.survival_rate * 100) + "%" : "n/a"} rate)`,
|
|
24113
|
+
total_applied_24h: summary.total_applied,
|
|
24114
|
+
survived_24h: summary.survived,
|
|
24115
|
+
removed_24h: summary.removed,
|
|
24116
|
+
files_24h: summary.files_touched,
|
|
24117
|
+
recent: summary.recent
|
|
24118
|
+
};
|
|
24119
|
+
return {
|
|
24120
|
+
name: "proactive_linking",
|
|
24121
|
+
priority: 6,
|
|
24122
|
+
content,
|
|
24123
|
+
estimated_tokens: estimateTokens2(content)
|
|
24124
|
+
};
|
|
24125
|
+
}
|
|
24024
24126
|
function registerBriefTools(server2, getStateDb4) {
|
|
24025
24127
|
server2.tool(
|
|
24026
24128
|
"brief",
|
|
@@ -24042,8 +24144,9 @@ function registerBriefTools(server2, getStateDb4) {
|
|
|
24042
24144
|
buildActiveEntitiesSection(stateDb2, 10),
|
|
24043
24145
|
buildActiveMemoriesSection(stateDb2, 20),
|
|
24044
24146
|
buildCorrectionsSection(stateDb2, 10),
|
|
24045
|
-
buildVaultPulseSection(stateDb2)
|
|
24046
|
-
|
|
24147
|
+
buildVaultPulseSection(stateDb2),
|
|
24148
|
+
buildProactiveLinkingSection(stateDb2)
|
|
24149
|
+
].filter((s) => s !== null);
|
|
24047
24150
|
if (args.max_tokens) {
|
|
24048
24151
|
let totalTokens2 = 0;
|
|
24049
24152
|
sections.sort((a, b) => a.priority - b.priority);
|
|
@@ -24441,7 +24544,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
|
|
|
24441
24544
|
await fs33.writeFile(fullPath, result.content, "utf-8");
|
|
24442
24545
|
notesModified++;
|
|
24443
24546
|
if (stateDb2) {
|
|
24444
|
-
trackWikilinkApplications(stateDb2, relativePath, entities);
|
|
24547
|
+
trackWikilinkApplications(stateDb2, relativePath, entities, "enrichment");
|
|
24445
24548
|
const newLinks = extractLinkedEntities(result.content);
|
|
24446
24549
|
updateStoredNoteLinks(stateDb2, relativePath, newLinks);
|
|
24447
24550
|
}
|
|
@@ -24595,7 +24698,7 @@ function registerActivityTools(server2, getStateDb4, getSessionId2) {
|
|
|
24595
24698
|
title: "Vault Activity",
|
|
24596
24699
|
description: "Use when checking what tools have been used and what notes have been accessed. Produces tool invocation records with session context and note paths. Returns activity entries filtered by tool name, session, or time range. Does not modify tracking data \u2014 read-only activity log.",
|
|
24597
24700
|
inputSchema: {
|
|
24598
|
-
mode: z31.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
|
|
24701
|
+
mode: z31.enum(["session", "sessions", "note_access", "tool_usage", "proactive_linking"]).describe("Activity query mode"),
|
|
24599
24702
|
session_id: z31.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
|
|
24600
24703
|
days_back: z31.number().optional().describe("Number of days to look back (default: 30)"),
|
|
24601
24704
|
limit: z31.number().optional().describe("Maximum results to return (default: 20)")
|
|
@@ -24659,6 +24762,24 @@ function registerActivityTools(server2, getStateDb4, getSessionId2) {
|
|
|
24659
24762
|
}, null, 2) }]
|
|
24660
24763
|
};
|
|
24661
24764
|
}
|
|
24765
|
+
case "proactive_linking": {
|
|
24766
|
+
const since = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1e3);
|
|
24767
|
+
const sinceStr = since.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
|
|
24768
|
+
const rows = stateDb2.db.prepare(
|
|
24769
|
+
`SELECT entity, note_path, applied_at, status, matched_term
|
|
24770
|
+
FROM wikilink_applications
|
|
24771
|
+
WHERE source = 'proactive' AND applied_at >= ?
|
|
24772
|
+
ORDER BY applied_at DESC LIMIT ?`
|
|
24773
|
+
).all(sinceStr, limit);
|
|
24774
|
+
return {
|
|
24775
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
24776
|
+
mode: "proactive_linking",
|
|
24777
|
+
days_back: daysBack,
|
|
24778
|
+
count: rows.length,
|
|
24779
|
+
applications: rows
|
|
24780
|
+
}, null, 2) }]
|
|
24781
|
+
};
|
|
24782
|
+
}
|
|
24662
24783
|
}
|
|
24663
24784
|
}
|
|
24664
24785
|
);
|
|
@@ -26171,6 +26292,25 @@ function getLearningReport(stateDb2, entityCount, linkCount, daysBack = 7, compa
|
|
|
26171
26292
|
funnel: queryFunnel(stateDb2, bounds.start, bounds.end, bounds.startMs, bounds.endMs),
|
|
26172
26293
|
graph: { link_count: linkCount, entity_count: entityCount }
|
|
26173
26294
|
};
|
|
26295
|
+
const sourceRows = stateDb2.db.prepare(`
|
|
26296
|
+
SELECT source,
|
|
26297
|
+
COUNT(*) as total_applied,
|
|
26298
|
+
SUM(CASE WHEN status = 'applied' THEN 1 ELSE 0 END) as survived,
|
|
26299
|
+
SUM(CASE WHEN status = 'removed' THEN 1 ELSE 0 END) as removed
|
|
26300
|
+
FROM wikilink_applications
|
|
26301
|
+
WHERE applied_at >= ? AND applied_at <= ?
|
|
26302
|
+
GROUP BY source
|
|
26303
|
+
ORDER BY total_applied DESC
|
|
26304
|
+
`).all(bounds.start, bounds.end);
|
|
26305
|
+
if (sourceRows.length > 0) {
|
|
26306
|
+
report.source_breakdown = sourceRows.map((r) => ({
|
|
26307
|
+
source: r.source,
|
|
26308
|
+
total_applied: r.total_applied,
|
|
26309
|
+
survived: r.survived,
|
|
26310
|
+
removed: r.removed,
|
|
26311
|
+
survival_rate: r.total_applied > 0 ? r.survived / r.total_applied : null
|
|
26312
|
+
}));
|
|
26313
|
+
}
|
|
26174
26314
|
const toolSelection = getToolSelectionReport(stateDb2, daysBack);
|
|
26175
26315
|
if (toolSelection) {
|
|
26176
26316
|
report.tool_selection = toolSelection;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "MCP tools that search, write, and auto-link your Obsidian vault — and learn from your edits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@huggingface/transformers": "^3.8.1",
|
|
57
57
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
58
|
-
"@velvetmonkey/vault-core": "^2.4.
|
|
58
|
+
"@velvetmonkey/vault-core": "^2.4.2",
|
|
59
59
|
"better-sqlite3": "^12.0.0",
|
|
60
60
|
"chokidar": "^4.0.0",
|
|
61
61
|
"gray-matter": "^4.0.3",
|