@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.
Files changed (2) hide show
  1. package/dist/index.js +154 -14
  2. 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 = indexBuilt ? index.entities.size : 0;
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.0",
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.0",
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",