@velvetmonkey/flywheel-memory 2.4.1 → 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 +145 -12
- 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 {
|
|
@@ -24028,6 +24105,24 @@ function buildVaultPulseSection(stateDb2) {
|
|
|
24028
24105
|
estimated_tokens: estimateTokens2(content)
|
|
24029
24106
|
};
|
|
24030
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
|
+
}
|
|
24031
24126
|
function registerBriefTools(server2, getStateDb4) {
|
|
24032
24127
|
server2.tool(
|
|
24033
24128
|
"brief",
|
|
@@ -24049,8 +24144,9 @@ function registerBriefTools(server2, getStateDb4) {
|
|
|
24049
24144
|
buildActiveEntitiesSection(stateDb2, 10),
|
|
24050
24145
|
buildActiveMemoriesSection(stateDb2, 20),
|
|
24051
24146
|
buildCorrectionsSection(stateDb2, 10),
|
|
24052
|
-
buildVaultPulseSection(stateDb2)
|
|
24053
|
-
|
|
24147
|
+
buildVaultPulseSection(stateDb2),
|
|
24148
|
+
buildProactiveLinkingSection(stateDb2)
|
|
24149
|
+
].filter((s) => s !== null);
|
|
24054
24150
|
if (args.max_tokens) {
|
|
24055
24151
|
let totalTokens2 = 0;
|
|
24056
24152
|
sections.sort((a, b) => a.priority - b.priority);
|
|
@@ -24448,7 +24544,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
|
|
|
24448
24544
|
await fs33.writeFile(fullPath, result.content, "utf-8");
|
|
24449
24545
|
notesModified++;
|
|
24450
24546
|
if (stateDb2) {
|
|
24451
|
-
trackWikilinkApplications(stateDb2, relativePath, entities);
|
|
24547
|
+
trackWikilinkApplications(stateDb2, relativePath, entities, "enrichment");
|
|
24452
24548
|
const newLinks = extractLinkedEntities(result.content);
|
|
24453
24549
|
updateStoredNoteLinks(stateDb2, relativePath, newLinks);
|
|
24454
24550
|
}
|
|
@@ -24602,7 +24698,7 @@ function registerActivityTools(server2, getStateDb4, getSessionId2) {
|
|
|
24602
24698
|
title: "Vault Activity",
|
|
24603
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.",
|
|
24604
24700
|
inputSchema: {
|
|
24605
|
-
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"),
|
|
24606
24702
|
session_id: z31.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
|
|
24607
24703
|
days_back: z31.number().optional().describe("Number of days to look back (default: 30)"),
|
|
24608
24704
|
limit: z31.number().optional().describe("Maximum results to return (default: 20)")
|
|
@@ -24666,6 +24762,24 @@ function registerActivityTools(server2, getStateDb4, getSessionId2) {
|
|
|
24666
24762
|
}, null, 2) }]
|
|
24667
24763
|
};
|
|
24668
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
|
+
}
|
|
24669
24783
|
}
|
|
24670
24784
|
}
|
|
24671
24785
|
);
|
|
@@ -26178,6 +26292,25 @@ function getLearningReport(stateDb2, entityCount, linkCount, daysBack = 7, compa
|
|
|
26178
26292
|
funnel: queryFunnel(stateDb2, bounds.start, bounds.end, bounds.startMs, bounds.endMs),
|
|
26179
26293
|
graph: { link_count: linkCount, entity_count: entityCount }
|
|
26180
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
|
+
}
|
|
26181
26314
|
const toolSelection = getToolSelectionReport(stateDb2, daysBack);
|
|
26182
26315
|
if (toolSelection) {
|
|
26183
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",
|