@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.
Files changed (2) hide show
  1. package/dist/index.js +145 -12
  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 {
@@ -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.1",
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.1",
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",