@velvetmonkey/flywheel-memory 2.0.33 → 2.0.34

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 +373 -7
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2487,7 +2487,10 @@ var DEFAULT_CONFIG = {
2487
2487
  exclude_task_tags: [],
2488
2488
  exclude_analysis_tags: [],
2489
2489
  exclude_entities: [],
2490
- exclude_entity_folders: []
2490
+ exclude_entity_folders: [],
2491
+ wikilink_strictness: "balanced",
2492
+ implicit_detection: true,
2493
+ adaptive_strictness: true
2491
2494
  };
2492
2495
  function loadConfig(stateDb2) {
2493
2496
  if (stateDb2) {
@@ -4902,6 +4905,24 @@ function setWriteStateDb(stateDb2) {
4902
4905
  function getWriteStateDb() {
4903
4906
  return moduleStateDb4;
4904
4907
  }
4908
+ var moduleConfig = null;
4909
+ var ALL_IMPLICIT_PATTERNS = ["proper-nouns", "single-caps", "quoted-terms", "camel-case", "acronyms"];
4910
+ function setWikilinkConfig(config) {
4911
+ moduleConfig = config;
4912
+ }
4913
+ function getWikilinkStrictness() {
4914
+ return moduleConfig?.wikilink_strictness ?? "balanced";
4915
+ }
4916
+ function getEffectiveStrictness(notePath) {
4917
+ const base = getWikilinkStrictness();
4918
+ if (moduleConfig?.adaptive_strictness === false) return base;
4919
+ const context = notePath ? getNoteContext(notePath) : "general";
4920
+ if (context === "daily") return "aggressive";
4921
+ return base;
4922
+ }
4923
+ function getCooccurrenceIndex() {
4924
+ return cooccurrenceIndex;
4925
+ }
4905
4926
  var entityIndex = null;
4906
4927
  var indexReady = false;
4907
4928
  var indexError2 = null;
@@ -5062,9 +5083,11 @@ function processWikilinks(content, notePath) {
5062
5083
  firstOccurrenceOnly: true,
5063
5084
  caseInsensitive: true
5064
5085
  });
5086
+ const implicitEnabled = moduleConfig?.implicit_detection !== false;
5087
+ const implicitPatterns = moduleConfig?.implicit_patterns?.length ? moduleConfig.implicit_patterns : [...ALL_IMPLICIT_PATTERNS];
5065
5088
  const implicitMatches = detectImplicitEntities(result.content, {
5066
- detectImplicit: true,
5067
- implicitPatterns: ["proper-nouns", "single-caps", "quoted-terms", "camel-case", "acronyms"],
5089
+ detectImplicit: implicitEnabled,
5090
+ implicitPatterns,
5068
5091
  minEntityLength: 3
5069
5092
  });
5070
5093
  const alreadyLinked = new Set(
@@ -5294,7 +5317,6 @@ var STRICTNESS_CONFIGS = {
5294
5317
  // Standard bonus for exact matches
5295
5318
  }
5296
5319
  };
5297
- var DEFAULT_STRICTNESS = "conservative";
5298
5320
  var TYPE_BOOST = {
5299
5321
  people: 5,
5300
5322
  // Names are high value for connections
@@ -5481,7 +5503,7 @@ async function suggestRelatedLinks(content, options = {}) {
5481
5503
  const {
5482
5504
  maxSuggestions = 3,
5483
5505
  excludeLinked = true,
5484
- strictness = DEFAULT_STRICTNESS,
5506
+ strictness = getEffectiveStrictness(options.notePath),
5485
5507
  notePath,
5486
5508
  detail = false
5487
5509
  } = options;
@@ -6046,6 +6068,17 @@ function searchFTS5(_vaultPath, query, limit = 10) {
6046
6068
  function getFTS5State() {
6047
6069
  return { ...state };
6048
6070
  }
6071
+ function countFTS5Mentions(term) {
6072
+ if (!db2) return 0;
6073
+ try {
6074
+ const result = db2.prepare(
6075
+ "SELECT COUNT(*) as cnt FROM notes_fts WHERE content MATCH ?"
6076
+ ).get(`"${term}"`);
6077
+ return result?.cnt ?? 0;
6078
+ } catch {
6079
+ return 0;
6080
+ }
6081
+ }
6049
6082
 
6050
6083
  // src/core/read/taskCache.ts
6051
6084
  import * as path10 from "path";
@@ -7057,12 +7090,13 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
7057
7090
  inputSchema: {
7058
7091
  path: z2.string().optional().describe("Path to a specific note to validate. If omitted, validates all notes."),
7059
7092
  typos_only: z2.boolean().default(false).describe("If true, only report broken links that have a similar existing note (likely typos)"),
7093
+ group_by_target: z2.boolean().default(false).describe("If true, aggregate dead links by target and rank by mention frequency. Returns targets[] instead of broken[]."),
7060
7094
  limit: z2.coerce.number().default(50).describe("Maximum number of broken links to return"),
7061
7095
  offset: z2.coerce.number().default(0).describe("Number of broken links to skip (for pagination)")
7062
7096
  },
7063
7097
  outputSchema: ValidateLinksOutputSchema
7064
7098
  },
7065
- async ({ path: notePath, typos_only, limit: requestedLimit, offset }) => {
7099
+ async ({ path: notePath, typos_only, group_by_target, limit: requestedLimit, offset }) => {
7066
7100
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
7067
7101
  const index = getIndex();
7068
7102
  const allBroken = [];
@@ -7103,6 +7137,41 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
7103
7137
  }
7104
7138
  }
7105
7139
  }
7140
+ if (group_by_target) {
7141
+ const targetMap = /* @__PURE__ */ new Map();
7142
+ for (const broken2 of allBroken) {
7143
+ const key = broken2.target.toLowerCase();
7144
+ const existing = targetMap.get(key);
7145
+ if (existing) {
7146
+ existing.count++;
7147
+ if (existing.sources.size < 5) existing.sources.add(broken2.source);
7148
+ if (!existing.suggestion && broken2.suggestion) existing.suggestion = broken2.suggestion;
7149
+ } else {
7150
+ targetMap.set(key, {
7151
+ count: 1,
7152
+ sources: /* @__PURE__ */ new Set([broken2.source]),
7153
+ suggestion: broken2.suggestion
7154
+ });
7155
+ }
7156
+ }
7157
+ const targets = Array.from(targetMap.entries()).map(([target, data]) => ({
7158
+ target,
7159
+ mention_count: data.count,
7160
+ sources: Array.from(data.sources),
7161
+ ...data.suggestion ? { suggestion: data.suggestion } : {}
7162
+ })).sort((a, b) => b.mention_count - a.mention_count).slice(offset, offset + limit);
7163
+ const grouped = {
7164
+ scope: notePath || "all",
7165
+ total_dead_targets: targetMap.size,
7166
+ total_broken_links: allBroken.length,
7167
+ returned_count: targets.length,
7168
+ targets
7169
+ };
7170
+ return {
7171
+ content: [{ type: "text", text: JSON.stringify(grouped, null, 2) }],
7172
+ structuredContent: grouped
7173
+ };
7174
+ }
7106
7175
  const broken = allBroken.slice(offset, offset + limit);
7107
7176
  const output = {
7108
7177
  scope: notePath || "all",
@@ -7123,6 +7192,106 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
7123
7192
  };
7124
7193
  }
7125
7194
  );
7195
+ server2.registerTool(
7196
+ "discover_stub_candidates",
7197
+ {
7198
+ title: "Discover Stub Candidates",
7199
+ description: `Find terms referenced via dead wikilinks across the vault that have no backing note. These are "invisible concepts" \u2014 topics your vault considers important enough to link to but that don't have their own notes yet. Ranked by reference frequency.`,
7200
+ inputSchema: {
7201
+ min_frequency: z2.coerce.number().default(2).describe("Minimum number of references to include (default 2)"),
7202
+ limit: z2.coerce.number().default(20).describe("Maximum candidates to return (default 20)")
7203
+ }
7204
+ },
7205
+ async ({ min_frequency, limit: requestedLimit }) => {
7206
+ const index = getIndex();
7207
+ const limit = Math.min(requestedLimit ?? 20, 100);
7208
+ const minFreq = min_frequency ?? 2;
7209
+ const targetMap = /* @__PURE__ */ new Map();
7210
+ for (const note of index.notes.values()) {
7211
+ for (const link of note.outlinks) {
7212
+ if (!resolveTarget(index, link.target)) {
7213
+ const key = link.target.toLowerCase();
7214
+ const existing = targetMap.get(key);
7215
+ if (existing) {
7216
+ existing.count++;
7217
+ if (existing.sources.size < 3) existing.sources.add(note.path);
7218
+ } else {
7219
+ targetMap.set(key, { count: 1, sources: /* @__PURE__ */ new Set([note.path]) });
7220
+ }
7221
+ }
7222
+ }
7223
+ }
7224
+ const candidates = Array.from(targetMap.entries()).filter(([, data]) => data.count >= minFreq).map(([target, data]) => {
7225
+ const fts5Mentions = countFTS5Mentions(target);
7226
+ return {
7227
+ term: target,
7228
+ wikilink_references: data.count,
7229
+ content_mentions: fts5Mentions,
7230
+ sample_notes: Array.from(data.sources)
7231
+ };
7232
+ }).sort((a, b) => b.wikilink_references - a.wikilink_references).slice(0, limit);
7233
+ const output = {
7234
+ total_dead_targets: targetMap.size,
7235
+ candidates_above_threshold: candidates.length,
7236
+ candidates
7237
+ };
7238
+ return {
7239
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
7240
+ };
7241
+ }
7242
+ );
7243
+ server2.registerTool(
7244
+ "discover_cooccurrence_gaps",
7245
+ {
7246
+ title: "Discover Co-occurrence Gaps",
7247
+ description: "Find entity pairs that frequently co-occur across vault notes but where one or both entities lack a backing note. These represent relationship patterns worth making explicit with hub notes or links.",
7248
+ inputSchema: {
7249
+ min_cooccurrence: z2.coerce.number().default(3).describe("Minimum co-occurrence count to include (default 3)"),
7250
+ limit: z2.coerce.number().default(20).describe("Maximum gaps to return (default 20)")
7251
+ }
7252
+ },
7253
+ async ({ min_cooccurrence, limit: requestedLimit }) => {
7254
+ const index = getIndex();
7255
+ const coocIndex = getCooccurrenceIndex();
7256
+ const limit = Math.min(requestedLimit ?? 20, 100);
7257
+ const minCount = min_cooccurrence ?? 3;
7258
+ if (!coocIndex) {
7259
+ return {
7260
+ content: [{ type: "text", text: JSON.stringify({ error: "Co-occurrence index not built yet. Wait for entity index initialization." }) }]
7261
+ };
7262
+ }
7263
+ const gaps = [];
7264
+ const seenPairs = /* @__PURE__ */ new Set();
7265
+ for (const [entityA, associations] of Object.entries(coocIndex.associations)) {
7266
+ for (const [entityB, count] of associations) {
7267
+ if (count < minCount) continue;
7268
+ const pairKey = [entityA, entityB].sort().join("||");
7269
+ if (seenPairs.has(pairKey)) continue;
7270
+ seenPairs.add(pairKey);
7271
+ const aHasNote = resolveTarget(index, entityA) !== null;
7272
+ const bHasNote = resolveTarget(index, entityB) !== null;
7273
+ if (aHasNote && bHasNote) continue;
7274
+ gaps.push({
7275
+ entity_a: entityA,
7276
+ entity_b: entityB,
7277
+ cooccurrence_count: count,
7278
+ a_has_note: aHasNote,
7279
+ b_has_note: bHasNote
7280
+ });
7281
+ }
7282
+ }
7283
+ gaps.sort((a, b) => b.cooccurrence_count - a.cooccurrence_count);
7284
+ const top = gaps.slice(0, limit);
7285
+ const output = {
7286
+ total_gaps: gaps.length,
7287
+ returned_count: top.length,
7288
+ gaps: top
7289
+ };
7290
+ return {
7291
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
7292
+ };
7293
+ }
7294
+ );
7126
7295
  }
7127
7296
 
7128
7297
  // src/tools/read/health.ts
@@ -7488,6 +7657,93 @@ function purgeOldIndexEvents(stateDb2, retentionDays = 90) {
7488
7657
  return result.changes;
7489
7658
  }
7490
7659
 
7660
+ // src/core/read/sweep.ts
7661
+ var DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
7662
+ var MIN_SWEEP_INTERVAL_MS = 30 * 1e3;
7663
+ var cachedResults = null;
7664
+ var sweepTimer = null;
7665
+ var sweepRunning = false;
7666
+ function runSweep(index) {
7667
+ const start = Date.now();
7668
+ let deadLinkCount = 0;
7669
+ const deadTargetCounts = /* @__PURE__ */ new Map();
7670
+ for (const note of index.notes.values()) {
7671
+ for (const link of note.outlinks) {
7672
+ if (!resolveTarget(index, link.target)) {
7673
+ deadLinkCount++;
7674
+ const key = link.target.toLowerCase();
7675
+ deadTargetCounts.set(key, (deadTargetCounts.get(key) || 0) + 1);
7676
+ }
7677
+ }
7678
+ }
7679
+ const topDeadTargets = Array.from(deadTargetCounts.entries()).filter(([, count]) => count >= 2).map(([target, wikilink_references]) => ({
7680
+ target,
7681
+ wikilink_references,
7682
+ content_mentions: countFTS5Mentions(target)
7683
+ })).sort((a, b) => b.wikilink_references - a.wikilink_references).slice(0, 10);
7684
+ const linkedCounts = /* @__PURE__ */ new Map();
7685
+ for (const note of index.notes.values()) {
7686
+ for (const link of note.outlinks) {
7687
+ const key = link.target.toLowerCase();
7688
+ linkedCounts.set(key, (linkedCounts.get(key) || 0) + 1);
7689
+ }
7690
+ }
7691
+ const seen = /* @__PURE__ */ new Set();
7692
+ const unlinkedEntities = [];
7693
+ for (const [name, entityPath] of index.entities) {
7694
+ if (seen.has(entityPath)) continue;
7695
+ seen.add(entityPath);
7696
+ const totalMentions = countFTS5Mentions(name);
7697
+ if (totalMentions === 0) continue;
7698
+ const pathKey = entityPath.toLowerCase().replace(/\.md$/, "");
7699
+ const linked = Math.max(linkedCounts.get(name) || 0, linkedCounts.get(pathKey) || 0);
7700
+ const unlinked = Math.max(0, totalMentions - linked - 1);
7701
+ if (unlinked <= 0) continue;
7702
+ const note = index.notes.get(entityPath);
7703
+ const displayName = note?.title || name;
7704
+ unlinkedEntities.push({ entity: displayName, path: entityPath, unlinked_mentions: unlinked });
7705
+ }
7706
+ unlinkedEntities.sort((a, b) => b.unlinked_mentions - a.unlinked_mentions);
7707
+ const results = {
7708
+ last_sweep_at: Date.now(),
7709
+ sweep_duration_ms: Date.now() - start,
7710
+ dead_link_count: deadLinkCount,
7711
+ top_dead_targets: topDeadTargets,
7712
+ top_unlinked_entities: unlinkedEntities.slice(0, 10)
7713
+ };
7714
+ cachedResults = results;
7715
+ return results;
7716
+ }
7717
+ function startSweepTimer(getIndex, intervalMs) {
7718
+ const interval = Math.max(intervalMs ?? DEFAULT_SWEEP_INTERVAL_MS, MIN_SWEEP_INTERVAL_MS);
7719
+ setTimeout(() => {
7720
+ doSweep(getIndex);
7721
+ }, 5e3);
7722
+ sweepTimer = setInterval(() => {
7723
+ doSweep(getIndex);
7724
+ }, interval);
7725
+ if (sweepTimer && typeof sweepTimer === "object" && "unref" in sweepTimer) {
7726
+ sweepTimer.unref();
7727
+ }
7728
+ }
7729
+ function doSweep(getIndex) {
7730
+ if (sweepRunning) return;
7731
+ sweepRunning = true;
7732
+ try {
7733
+ const index = getIndex();
7734
+ if (index && index.notes && index.notes.size > 0) {
7735
+ runSweep(index);
7736
+ }
7737
+ } catch (err) {
7738
+ console.error("[Flywheel] Sweep error:", err);
7739
+ } finally {
7740
+ sweepRunning = false;
7741
+ }
7742
+ }
7743
+ function getSweepResults() {
7744
+ return cachedResults;
7745
+ }
7746
+
7491
7747
  // src/tools/read/health.ts
7492
7748
  var STALE_THRESHOLD_SECONDS = 300;
7493
7749
  function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({}), getStateDb = () => null) {
@@ -7563,6 +7819,26 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7563
7819
  embeddings_count: z3.coerce.number().describe("Number of notes with semantic embeddings"),
7564
7820
  tasks_ready: z3.boolean().describe("Whether the task cache is ready to serve queries"),
7565
7821
  tasks_building: z3.boolean().describe("Whether the task cache is currently rebuilding"),
7822
+ dead_link_count: z3.coerce.number().describe("Total number of broken/dead wikilinks across the vault"),
7823
+ top_dead_link_targets: z3.array(z3.object({
7824
+ target: z3.string().describe("The dead link target"),
7825
+ mention_count: z3.coerce.number().describe("How many notes reference this dead target")
7826
+ })).describe("Top 5 most-referenced dead link targets (highest-ROI candidates to create)"),
7827
+ sweep: z3.object({
7828
+ last_sweep_at: z3.number().describe("When the last background sweep completed (ms epoch)"),
7829
+ sweep_duration_ms: z3.number().describe("How long the last sweep took"),
7830
+ dead_link_count: z3.number().describe("Dead links found by sweep"),
7831
+ top_dead_targets: z3.array(z3.object({
7832
+ target: z3.string(),
7833
+ wikilink_references: z3.number(),
7834
+ content_mentions: z3.number()
7835
+ })).describe("Top dead link targets with FTS5 content mention counts"),
7836
+ top_unlinked_entities: z3.array(z3.object({
7837
+ entity: z3.string(),
7838
+ path: z3.string(),
7839
+ unlinked_mentions: z3.number()
7840
+ })).describe("Entities with the most unlinked plain-text mentions")
7841
+ }).optional().describe("Background sweep results (graph hygiene metrics, updated every 5 min)"),
7566
7842
  recommendations: z3.array(z3.string()).describe("Suggested actions if any issues detected")
7567
7843
  };
7568
7844
  server2.registerTool(
@@ -7685,6 +7961,20 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7685
7961
  }
7686
7962
  }
7687
7963
  const ftsState = getFTS5State();
7964
+ let deadLinkCount = 0;
7965
+ const deadTargetCounts = /* @__PURE__ */ new Map();
7966
+ if (indexBuilt) {
7967
+ for (const note of index.notes.values()) {
7968
+ for (const link of note.outlinks) {
7969
+ if (!resolveTarget(index, link.target)) {
7970
+ deadLinkCount++;
7971
+ const key = link.target.toLowerCase();
7972
+ deadTargetCounts.set(key, (deadTargetCounts.get(key) || 0) + 1);
7973
+ }
7974
+ }
7975
+ }
7976
+ }
7977
+ const topDeadLinkTargets = Array.from(deadTargetCounts.entries()).map(([target, mention_count]) => ({ target, mention_count })).sort((a, b) => b.mention_count - a.mention_count).slice(0, 5);
7688
7978
  const output = {
7689
7979
  status,
7690
7980
  schema_version: SCHEMA_VERSION,
@@ -7712,6 +8002,9 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7712
8002
  embeddings_count: getEmbeddingsCount(),
7713
8003
  tasks_ready: isTaskCacheReady(),
7714
8004
  tasks_building: isTaskCacheBuilding(),
8005
+ dead_link_count: deadLinkCount,
8006
+ top_dead_link_targets: topDeadLinkTargets,
8007
+ sweep: getSweepResults() ?? void 0,
7715
8008
  recommendations
7716
8009
  };
7717
8010
  return {
@@ -8205,6 +8498,14 @@ function generateAliasCandidates(entityName, existingAliases) {
8205
8498
  candidates.push({ candidate: short, type: "short_form" });
8206
8499
  }
8207
8500
  }
8501
+ const STOPWORDS2 = /* @__PURE__ */ new Set(["the", "and", "for", "with", "from", "into", "that", "this", "are", "was", "has", "its"]);
8502
+ for (const word of words) {
8503
+ if (word.length < 4) continue;
8504
+ if (STOPWORDS2.has(word.toLowerCase())) continue;
8505
+ if (existing.has(word.toLowerCase())) continue;
8506
+ if (words.length >= 3 && word === words[0]) continue;
8507
+ candidates.push({ candidate: word, type: "name_fragment" });
8508
+ }
8208
8509
  }
8209
8510
  return candidates;
8210
8511
  }
@@ -8231,6 +8532,7 @@ function suggestEntityAliases(stateDb2, folder) {
8231
8532
  mentions = result?.cnt ?? 0;
8232
8533
  } catch {
8233
8534
  }
8535
+ if (type === "name_fragment" && mentions < 5) continue;
8234
8536
  suggestions.push({
8235
8537
  entity: row.name,
8236
8538
  entity_path: row.path,
@@ -8761,6 +9063,61 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
8761
9063
  };
8762
9064
  }
8763
9065
  );
9066
+ server2.registerTool(
9067
+ "unlinked_mentions_report",
9068
+ {
9069
+ title: "Unlinked Mentions Report",
9070
+ description: "Find which entities have the most unlinked mentions across the vault \u2014 highest-ROI linking opportunities. Uses FTS5 to count mentions and subtracts known wikilinks.",
9071
+ inputSchema: {
9072
+ limit: z5.coerce.number().default(20).describe("Maximum entities to return (default 20)")
9073
+ }
9074
+ },
9075
+ async ({ limit: requestedLimit }) => {
9076
+ requireIndex();
9077
+ const index = getIndex();
9078
+ const limit = Math.min(requestedLimit ?? 20, 100);
9079
+ const linkedCounts = /* @__PURE__ */ new Map();
9080
+ for (const note of index.notes.values()) {
9081
+ for (const link of note.outlinks) {
9082
+ const key = link.target.toLowerCase();
9083
+ linkedCounts.set(key, (linkedCounts.get(key) || 0) + 1);
9084
+ }
9085
+ }
9086
+ const results = [];
9087
+ const seen = /* @__PURE__ */ new Set();
9088
+ for (const [name, entityPath] of index.entities) {
9089
+ if (seen.has(entityPath)) continue;
9090
+ seen.add(entityPath);
9091
+ const totalMentions = countFTS5Mentions(name);
9092
+ if (totalMentions === 0) continue;
9093
+ const pathKey = entityPath.toLowerCase().replace(/\.md$/, "");
9094
+ const linkedByName = linkedCounts.get(name) || 0;
9095
+ const linkedByPath = linkedCounts.get(pathKey) || 0;
9096
+ const linked = Math.max(linkedByName, linkedByPath);
9097
+ const unlinked = Math.max(0, totalMentions - linked - 1);
9098
+ if (unlinked <= 0) continue;
9099
+ const note = index.notes.get(entityPath);
9100
+ const displayName = note?.title || name;
9101
+ results.push({
9102
+ entity: displayName,
9103
+ path: entityPath,
9104
+ total_mentions: totalMentions,
9105
+ linked_mentions: linked,
9106
+ unlinked_mentions: unlinked
9107
+ });
9108
+ }
9109
+ results.sort((a, b) => b.unlinked_mentions - a.unlinked_mentions);
9110
+ const top = results.slice(0, limit);
9111
+ const output = {
9112
+ total_entities_checked: seen.size,
9113
+ entities_with_unlinked: results.length,
9114
+ top_entities: top
9115
+ };
9116
+ return {
9117
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
9118
+ };
9119
+ }
9120
+ );
8764
9121
  }
8765
9122
 
8766
9123
  // src/tools/read/primitives.ts
@@ -12119,8 +12476,12 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
12119
12476
  return formatMcpResult(errorResult(notePath, `Template not found: ${template}`));
12120
12477
  }
12121
12478
  }
12479
+ const now = /* @__PURE__ */ new Date();
12122
12480
  if (!effectiveFrontmatter.date) {
12123
- effectiveFrontmatter.date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
12481
+ effectiveFrontmatter.date = now.toISOString().split("T")[0];
12482
+ }
12483
+ if (!effectiveFrontmatter.created) {
12484
+ effectiveFrontmatter.created = now.toISOString();
12124
12485
  }
12125
12486
  const warnings = [];
12126
12487
  const noteName = path21.basename(notePath, ".md");
@@ -15931,6 +16292,7 @@ registerSystemTools(
15931
16292
  () => vaultPath,
15932
16293
  (newConfig) => {
15933
16294
  flywheelConfig = newConfig;
16295
+ setWikilinkConfig(newConfig);
15934
16296
  },
15935
16297
  () => stateDb
15936
16298
  );
@@ -15957,6 +16319,7 @@ registerConfigTools(
15957
16319
  () => flywheelConfig,
15958
16320
  (newConfig) => {
15959
16321
  flywheelConfig = newConfig;
16322
+ setWikilinkConfig(newConfig);
15960
16323
  },
15961
16324
  () => stateDb
15962
16325
  );
@@ -16139,6 +16502,7 @@ async function runPostIndexWork(index) {
16139
16502
  saveConfig(stateDb, inferred, existing);
16140
16503
  }
16141
16504
  flywheelConfig = loadConfig(stateDb);
16505
+ setWikilinkConfig(flywheelConfig);
16142
16506
  const configKeys = Object.keys(flywheelConfig).filter((k) => flywheelConfig[k] != null);
16143
16507
  serverLog("config", `Config inferred: ${configKeys.join(", ")}`);
16144
16508
  if (stateDb) {
@@ -16376,6 +16740,8 @@ async function runPostIndexWork(index) {
16376
16740
  watcher.start();
16377
16741
  serverLog("watcher", "File watcher started");
16378
16742
  }
16743
+ startSweepTimer(() => vaultIndex);
16744
+ serverLog("server", "Sweep timer started (5 min interval)");
16379
16745
  const postDuration = Date.now() - postStart;
16380
16746
  serverLog("server", `Post-index work complete in ${postDuration}ms`);
16381
16747
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.33",
3
+ "version": "2.0.34",
4
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.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@modelcontextprotocol/sdk": "^1.25.1",
53
- "@velvetmonkey/vault-core": "^2.0.33",
53
+ "@velvetmonkey/vault-core": "^2.0.34",
54
54
  "better-sqlite3": "^11.0.0",
55
55
  "chokidar": "^4.0.0",
56
56
  "gray-matter": "^4.0.3",