@velvetmonkey/flywheel-memory 2.0.16 → 2.0.18

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 +218 -85
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1426,7 +1426,6 @@ var init_taskHelpers = __esm({
1426
1426
  // src/index.ts
1427
1427
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1428
1428
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1429
- import chokidar2 from "chokidar";
1430
1429
 
1431
1430
  // src/core/read/vault.ts
1432
1431
  import * as fs from "fs";
@@ -6187,9 +6186,84 @@ function getActivitySummary(index, days) {
6187
6186
  };
6188
6187
  }
6189
6188
 
6189
+ // src/core/shared/indexActivity.ts
6190
+ function recordIndexEvent(stateDb2, event) {
6191
+ stateDb2.db.prepare(
6192
+ `INSERT INTO index_events (timestamp, trigger, duration_ms, success, note_count, files_changed, changed_paths, error)
6193
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
6194
+ ).run(
6195
+ Date.now(),
6196
+ event.trigger,
6197
+ event.duration_ms,
6198
+ event.success !== false ? 1 : 0,
6199
+ event.note_count ?? null,
6200
+ event.files_changed ?? null,
6201
+ event.changed_paths ? JSON.stringify(event.changed_paths) : null,
6202
+ event.error ?? null
6203
+ );
6204
+ }
6205
+ function rowToEvent(row) {
6206
+ return {
6207
+ id: row.id,
6208
+ timestamp: row.timestamp,
6209
+ trigger: row.trigger,
6210
+ duration_ms: row.duration_ms,
6211
+ success: row.success === 1,
6212
+ note_count: row.note_count,
6213
+ files_changed: row.files_changed,
6214
+ changed_paths: row.changed_paths ? JSON.parse(row.changed_paths) : null,
6215
+ error: row.error
6216
+ };
6217
+ }
6218
+ function getRecentIndexEvents(stateDb2, limit = 20) {
6219
+ const rows = stateDb2.db.prepare(
6220
+ "SELECT * FROM index_events ORDER BY timestamp DESC LIMIT ?"
6221
+ ).all(limit);
6222
+ return rows.map(rowToEvent);
6223
+ }
6224
+ function getIndexActivitySummary(stateDb2) {
6225
+ const now = Date.now();
6226
+ const todayStart = /* @__PURE__ */ new Date();
6227
+ todayStart.setHours(0, 0, 0, 0);
6228
+ const last24h = now - 24 * 60 * 60 * 1e3;
6229
+ const totalRow = stateDb2.db.prepare(
6230
+ "SELECT COUNT(*) as count FROM index_events"
6231
+ ).get();
6232
+ const todayRow = stateDb2.db.prepare(
6233
+ "SELECT COUNT(*) as count FROM index_events WHERE timestamp >= ?"
6234
+ ).get(todayStart.getTime());
6235
+ const last24hRow = stateDb2.db.prepare(
6236
+ "SELECT COUNT(*) as count FROM index_events WHERE timestamp >= ?"
6237
+ ).get(last24h);
6238
+ const avgRow = stateDb2.db.prepare(
6239
+ "SELECT AVG(duration_ms) as avg_ms FROM index_events WHERE success = 1"
6240
+ ).get();
6241
+ const failureRow = stateDb2.db.prepare(
6242
+ "SELECT COUNT(*) as count FROM index_events WHERE success = 0"
6243
+ ).get();
6244
+ const lastRow = stateDb2.db.prepare(
6245
+ "SELECT * FROM index_events ORDER BY timestamp DESC LIMIT 1"
6246
+ ).get();
6247
+ return {
6248
+ total_rebuilds: totalRow.count,
6249
+ last_rebuild: lastRow ? rowToEvent(lastRow) : null,
6250
+ rebuilds_today: todayRow.count,
6251
+ rebuilds_last_24h: last24hRow.count,
6252
+ avg_duration_ms: Math.round(avgRow.avg_ms ?? 0),
6253
+ failure_count: failureRow.count
6254
+ };
6255
+ }
6256
+ function purgeOldIndexEvents(stateDb2, retentionDays = 90) {
6257
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
6258
+ const result = stateDb2.db.prepare(
6259
+ "DELETE FROM index_events WHERE timestamp < ?"
6260
+ ).run(cutoff);
6261
+ return result.changes;
6262
+ }
6263
+
6190
6264
  // src/tools/read/health.ts
6191
6265
  var STALE_THRESHOLD_SECONDS = 300;
6192
- function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({})) {
6266
+ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({}), getStateDb = () => null) {
6193
6267
  const IndexProgressSchema = z3.object({
6194
6268
  parsed: z3.coerce.number().describe("Number of files parsed so far"),
6195
6269
  total: z3.coerce.number().describe("Total number of files to parse")
@@ -6217,6 +6291,12 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6217
6291
  tag_count: z3.coerce.number().describe("Number of unique tags"),
6218
6292
  periodic_notes: z3.array(PeriodicNoteInfoSchema).optional().describe("Detected periodic note conventions"),
6219
6293
  config: z3.record(z3.unknown()).optional().describe("Current flywheel config (paths, templates, etc.)"),
6294
+ last_rebuild: z3.object({
6295
+ trigger: z3.string(),
6296
+ timestamp: z3.number(),
6297
+ duration_ms: z3.number(),
6298
+ ago_seconds: z3.number()
6299
+ }).optional().describe("Most recent index rebuild event"),
6220
6300
  recommendations: z3.array(z3.string()).describe("Suggested actions if any issues detected")
6221
6301
  };
6222
6302
  server2.registerTool(
@@ -6285,6 +6365,23 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6285
6365
  }
6286
6366
  const config = getConfig();
6287
6367
  const configInfo = Object.keys(config).length > 0 ? config : void 0;
6368
+ let lastRebuild;
6369
+ const stateDb2 = getStateDb();
6370
+ if (stateDb2) {
6371
+ try {
6372
+ const events = getRecentIndexEvents(stateDb2, 1);
6373
+ if (events.length > 0) {
6374
+ const event = events[0];
6375
+ lastRebuild = {
6376
+ trigger: event.trigger,
6377
+ timestamp: event.timestamp,
6378
+ duration_ms: event.duration_ms,
6379
+ ago_seconds: Math.floor((Date.now() - event.timestamp) / 1e3)
6380
+ };
6381
+ }
6382
+ } catch {
6383
+ }
6384
+ }
6288
6385
  const output = {
6289
6386
  status,
6290
6387
  vault_accessible: vaultAccessible,
@@ -6300,6 +6397,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6300
6397
  tag_count: tagCount,
6301
6398
  periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
6302
6399
  config: configInfo,
6400
+ last_rebuild: lastRebuild,
6303
6401
  recommendations
6304
6402
  };
6305
6403
  return {
@@ -6737,12 +6835,20 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
6737
6835
  } catch (err) {
6738
6836
  console.error("[Flywheel] FTS5 rebuild failed:", err);
6739
6837
  }
6838
+ const duration = Date.now() - startTime;
6839
+ if (stateDb2) {
6840
+ recordIndexEvent(stateDb2, {
6841
+ trigger: "manual_refresh",
6842
+ duration_ms: duration,
6843
+ note_count: newIndex.notes.size
6844
+ });
6845
+ }
6740
6846
  const output = {
6741
6847
  success: true,
6742
6848
  notes_count: newIndex.notes.size,
6743
6849
  entities_count: newIndex.entities.size,
6744
6850
  fts5_notes: fts5Notes,
6745
- duration_ms: Date.now() - startTime
6851
+ duration_ms: duration
6746
6852
  };
6747
6853
  return {
6748
6854
  content: [
@@ -6756,12 +6862,22 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
6756
6862
  } catch (err) {
6757
6863
  setIndexState("error");
6758
6864
  setIndexError(err instanceof Error ? err : new Error(String(err)));
6865
+ const duration = Date.now() - startTime;
6866
+ const stateDb2 = getStateDb?.();
6867
+ if (stateDb2) {
6868
+ recordIndexEvent(stateDb2, {
6869
+ trigger: "manual_refresh",
6870
+ duration_ms: duration,
6871
+ success: false,
6872
+ error: err instanceof Error ? err.message : String(err)
6873
+ });
6874
+ }
6759
6875
  const output = {
6760
6876
  success: false,
6761
6877
  notes_count: 0,
6762
6878
  entities_count: 0,
6763
6879
  fts5_notes: 0,
6764
- duration_ms: Date.now() - startTime
6880
+ duration_ms: duration
6765
6881
  };
6766
6882
  return {
6767
6883
  content: [
@@ -12377,14 +12493,15 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
12377
12493
  "vault_growth",
12378
12494
  {
12379
12495
  title: "Vault Growth",
12380
- description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago). Tracks 11 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio, wikilink_accuracy, wikilink_feedback_volume, wikilink_suppressed_count.',
12496
+ description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago), "index_activity" (rebuild history). Tracks 11 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio, wikilink_accuracy, wikilink_feedback_volume, wikilink_suppressed_count.',
12381
12497
  inputSchema: {
12382
- mode: z21.enum(["current", "history", "trends"]).describe("Query mode: current snapshot, historical time series, or trend analysis"),
12498
+ mode: z21.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
12383
12499
  metric: z21.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
12384
- days_back: z21.number().optional().describe("Number of days to look back for history/trends (default: 30)")
12500
+ days_back: z21.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
12501
+ limit: z21.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
12385
12502
  }
12386
12503
  },
12387
- async ({ mode, metric, days_back }) => {
12504
+ async ({ mode, metric, days_back, limit: eventLimit }) => {
12388
12505
  const index = getIndex();
12389
12506
  const stateDb2 = getStateDb();
12390
12507
  const daysBack = days_back ?? 30;
@@ -12426,6 +12543,20 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
12426
12543
  };
12427
12544
  break;
12428
12545
  }
12546
+ case "index_activity": {
12547
+ if (!stateDb2) {
12548
+ return {
12549
+ content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for index activity queries" }) }]
12550
+ };
12551
+ }
12552
+ const summary = getIndexActivitySummary(stateDb2);
12553
+ const recentEvents = getRecentIndexEvents(stateDb2, eventLimit ?? 20);
12554
+ result = {
12555
+ mode: "index_activity",
12556
+ index_activity: { summary, recent_events: recentEvents }
12557
+ };
12558
+ break;
12559
+ }
12429
12560
  }
12430
12561
  return {
12431
12562
  content: [
@@ -12709,7 +12840,7 @@ if (_originalRegisterTool) {
12709
12840
  }
12710
12841
  var categoryList = Array.from(enabledCategories).sort().join(", ");
12711
12842
  console.error(`[Memory] Tool categories: ${categoryList}`);
12712
- registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
12843
+ registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
12713
12844
  registerSystemTools(
12714
12845
  server,
12715
12846
  () => vaultIndex,
@@ -12786,6 +12917,13 @@ async function main() {
12786
12917
  setIndexState("ready");
12787
12918
  const duration = Date.now() - startTime;
12788
12919
  console.error(`[Memory] Index loaded from cache in ${duration}ms`);
12920
+ if (stateDb) {
12921
+ recordIndexEvent(stateDb, {
12922
+ trigger: "startup_cache",
12923
+ duration_ms: duration,
12924
+ note_count: cachedIndex.notes.size
12925
+ });
12926
+ }
12789
12927
  runPostIndexWork(vaultIndex);
12790
12928
  } else {
12791
12929
  console.error("[Memory] Building vault index...");
@@ -12794,6 +12932,13 @@ async function main() {
12794
12932
  setIndexState("ready");
12795
12933
  const duration = Date.now() - startTime;
12796
12934
  console.error(`[Memory] Vault index ready in ${duration}ms`);
12935
+ if (stateDb) {
12936
+ recordIndexEvent(stateDb, {
12937
+ trigger: "startup_build",
12938
+ duration_ms: duration,
12939
+ note_count: vaultIndex.notes.size
12940
+ });
12941
+ }
12797
12942
  if (stateDb) {
12798
12943
  try {
12799
12944
  saveVaultIndexToCache(stateDb, vaultIndex);
@@ -12806,6 +12951,15 @@ async function main() {
12806
12951
  } catch (err) {
12807
12952
  setIndexState("error");
12808
12953
  setIndexError(err instanceof Error ? err : new Error(String(err)));
12954
+ const duration = Date.now() - startTime;
12955
+ if (stateDb) {
12956
+ recordIndexEvent(stateDb, {
12957
+ trigger: "startup_build",
12958
+ duration_ms: duration,
12959
+ success: false,
12960
+ error: err instanceof Error ? err.message : String(err)
12961
+ });
12962
+ }
12809
12963
  console.error("[Memory] Failed to build vault index:", err);
12810
12964
  }
12811
12965
  }
@@ -12850,6 +13004,7 @@ async function runPostIndexWork(index) {
12850
13004
  const metrics = computeMetrics(index, stateDb);
12851
13005
  recordMetrics(stateDb, metrics);
12852
13006
  purgeOldMetrics(stateDb, 90);
13007
+ purgeOldIndexEvents(stateDb, 90);
12853
13008
  console.error("[Memory] Growth metrics recorded");
12854
13009
  } catch (err) {
12855
13010
  console.error("[Memory] Failed to record metrics:", err);
@@ -12872,87 +13027,65 @@ async function runPostIndexWork(index) {
12872
13027
  console.error(`[Memory] Vault: ${flywheelConfig.vault_name}`);
12873
13028
  }
12874
13029
  if (process.env.FLYWHEEL_WATCH !== "false") {
12875
- const useV2Watcher = process.env.FLYWHEEL_WATCH_V2 === "true" || process.env.FLYWHEEL_WATCH_POLL === "true";
12876
- if (useV2Watcher) {
12877
- const config = parseWatcherConfig();
12878
- console.error(`[Memory] File watcher v2 enabled (debounce: ${config.debounceMs}ms)`);
12879
- const watcher = createVaultWatcher({
12880
- vaultPath,
12881
- config,
12882
- onBatch: async (batch) => {
12883
- console.error(`[Memory] Processing ${batch.events.length} file changes`);
12884
- const startTime = Date.now();
12885
- try {
12886
- vaultIndex = await buildVaultIndex(vaultPath);
12887
- setIndexState("ready");
12888
- console.error(`[Memory] Index rebuilt in ${Date.now() - startTime}ms`);
12889
- await updateEntitiesInStateDb();
12890
- await exportHubScores(vaultIndex, stateDb);
12891
- if (stateDb) {
12892
- try {
12893
- saveVaultIndexToCache(stateDb, vaultIndex);
12894
- } catch (err) {
12895
- console.error("[Memory] Failed to update index cache:", err);
12896
- }
13030
+ const config = parseWatcherConfig();
13031
+ console.error(`[Memory] File watcher enabled (debounce: ${config.debounceMs}ms)`);
13032
+ const watcher = createVaultWatcher({
13033
+ vaultPath,
13034
+ config,
13035
+ onBatch: async (batch) => {
13036
+ console.error(`[Memory] Processing ${batch.events.length} file changes`);
13037
+ const batchStart = Date.now();
13038
+ const changedPaths = batch.events.map((e) => e.path);
13039
+ try {
13040
+ vaultIndex = await buildVaultIndex(vaultPath);
13041
+ setIndexState("ready");
13042
+ const duration = Date.now() - batchStart;
13043
+ console.error(`[Memory] Index rebuilt in ${duration}ms`);
13044
+ if (stateDb) {
13045
+ recordIndexEvent(stateDb, {
13046
+ trigger: "watcher",
13047
+ duration_ms: duration,
13048
+ note_count: vaultIndex.notes.size,
13049
+ files_changed: batch.events.length,
13050
+ changed_paths: changedPaths
13051
+ });
13052
+ }
13053
+ await updateEntitiesInStateDb();
13054
+ await exportHubScores(vaultIndex, stateDb);
13055
+ if (stateDb) {
13056
+ try {
13057
+ saveVaultIndexToCache(stateDb, vaultIndex);
13058
+ } catch (err) {
13059
+ console.error("[Memory] Failed to update index cache:", err);
12897
13060
  }
12898
- } catch (err) {
12899
- setIndexState("error");
12900
- setIndexError(err instanceof Error ? err : new Error(String(err)));
12901
- console.error("[Memory] Failed to rebuild index:", err);
12902
13061
  }
12903
- },
12904
- onStateChange: (status) => {
12905
- if (status.state === "dirty") {
12906
- console.error("[Memory] Warning: Index may be stale");
13062
+ } catch (err) {
13063
+ setIndexState("error");
13064
+ setIndexError(err instanceof Error ? err : new Error(String(err)));
13065
+ const duration = Date.now() - batchStart;
13066
+ if (stateDb) {
13067
+ recordIndexEvent(stateDb, {
13068
+ trigger: "watcher",
13069
+ duration_ms: duration,
13070
+ success: false,
13071
+ files_changed: batch.events.length,
13072
+ changed_paths: changedPaths,
13073
+ error: err instanceof Error ? err.message : String(err)
13074
+ });
12907
13075
  }
12908
- },
12909
- onError: (err) => {
12910
- console.error("[Memory] Watcher error:", err.message);
13076
+ console.error("[Memory] Failed to rebuild index:", err);
12911
13077
  }
12912
- });
12913
- watcher.start();
12914
- } else {
12915
- const debounceMs = parseInt(process.env.FLYWHEEL_DEBOUNCE_MS || "60000");
12916
- console.error(`[Memory] File watcher v1 enabled (debounce: ${debounceMs}ms)`);
12917
- if (debounceMs >= 6e4) {
12918
- console.error("[Memory] Warning: Legacy watcher using high debounce (60s). Set FLYWHEEL_WATCH_V2=true for 200ms responsiveness.");
12919
- }
12920
- const legacyWatcher = chokidar2.watch(vaultPath, {
12921
- ignored: /(^|[\/\\])\../,
12922
- persistent: true,
12923
- ignoreInitial: true,
12924
- awaitWriteFinish: {
12925
- stabilityThreshold: 300,
12926
- pollInterval: 100
13078
+ },
13079
+ onStateChange: (status) => {
13080
+ if (status.state === "dirty") {
13081
+ console.error("[Memory] Warning: Index may be stale");
12927
13082
  }
12928
- });
12929
- let rebuildTimer;
12930
- legacyWatcher.on("all", (event, path25) => {
12931
- if (!path25.endsWith(".md")) return;
12932
- clearTimeout(rebuildTimer);
12933
- rebuildTimer = setTimeout(() => {
12934
- console.error("[Memory] Rebuilding index (file changed)");
12935
- buildVaultIndex(vaultPath).then(async (newIndex) => {
12936
- vaultIndex = newIndex;
12937
- setIndexState("ready");
12938
- console.error("[Memory] Index rebuilt successfully");
12939
- await updateEntitiesInStateDb();
12940
- await exportHubScores(newIndex, stateDb);
12941
- if (stateDb) {
12942
- try {
12943
- saveVaultIndexToCache(stateDb, newIndex);
12944
- } catch (err) {
12945
- console.error("[Memory] Failed to update index cache:", err);
12946
- }
12947
- }
12948
- }).catch((err) => {
12949
- setIndexState("error");
12950
- setIndexError(err instanceof Error ? err : new Error(String(err)));
12951
- console.error("[Memory] Failed to rebuild index:", err);
12952
- });
12953
- }, debounceMs);
12954
- });
12955
- }
13083
+ },
13084
+ onError: (err) => {
13085
+ console.error("[Memory] Watcher error:", err.message);
13086
+ }
13087
+ });
13088
+ watcher.start();
12956
13089
  }
12957
13090
  }
12958
13091
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. 39 tools for search, backlinks, graph queries, and mutations.",
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.16",
53
+ "@velvetmonkey/vault-core": "^2.0.18",
54
54
  "better-sqlite3": "^11.0.0",
55
55
  "chokidar": "^4.0.0",
56
56
  "gray-matter": "^4.0.3",