@velvetmonkey/flywheel-memory 2.0.17 → 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 +188 -10
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6186,9 +6186,84 @@ function getActivitySummary(index, days) {
6186
6186
  };
6187
6187
  }
6188
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
+
6189
6264
  // src/tools/read/health.ts
6190
6265
  var STALE_THRESHOLD_SECONDS = 300;
6191
- function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({})) {
6266
+ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () => ({}), getStateDb = () => null) {
6192
6267
  const IndexProgressSchema = z3.object({
6193
6268
  parsed: z3.coerce.number().describe("Number of files parsed so far"),
6194
6269
  total: z3.coerce.number().describe("Total number of files to parse")
@@ -6216,6 +6291,12 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6216
6291
  tag_count: z3.coerce.number().describe("Number of unique tags"),
6217
6292
  periodic_notes: z3.array(PeriodicNoteInfoSchema).optional().describe("Detected periodic note conventions"),
6218
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"),
6219
6300
  recommendations: z3.array(z3.string()).describe("Suggested actions if any issues detected")
6220
6301
  };
6221
6302
  server2.registerTool(
@@ -6284,6 +6365,23 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6284
6365
  }
6285
6366
  const config = getConfig();
6286
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
+ }
6287
6385
  const output = {
6288
6386
  status,
6289
6387
  vault_accessible: vaultAccessible,
@@ -6299,6 +6397,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6299
6397
  tag_count: tagCount,
6300
6398
  periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
6301
6399
  config: configInfo,
6400
+ last_rebuild: lastRebuild,
6302
6401
  recommendations
6303
6402
  };
6304
6403
  return {
@@ -6736,12 +6835,20 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
6736
6835
  } catch (err) {
6737
6836
  console.error("[Flywheel] FTS5 rebuild failed:", err);
6738
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
+ }
6739
6846
  const output = {
6740
6847
  success: true,
6741
6848
  notes_count: newIndex.notes.size,
6742
6849
  entities_count: newIndex.entities.size,
6743
6850
  fts5_notes: fts5Notes,
6744
- duration_ms: Date.now() - startTime
6851
+ duration_ms: duration
6745
6852
  };
6746
6853
  return {
6747
6854
  content: [
@@ -6755,12 +6862,22 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
6755
6862
  } catch (err) {
6756
6863
  setIndexState("error");
6757
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
+ }
6758
6875
  const output = {
6759
6876
  success: false,
6760
6877
  notes_count: 0,
6761
6878
  entities_count: 0,
6762
6879
  fts5_notes: 0,
6763
- duration_ms: Date.now() - startTime
6880
+ duration_ms: duration
6764
6881
  };
6765
6882
  return {
6766
6883
  content: [
@@ -12376,14 +12493,15 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
12376
12493
  "vault_growth",
12377
12494
  {
12378
12495
  title: "Vault Growth",
12379
- 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.',
12380
12497
  inputSchema: {
12381
- 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"),
12382
12499
  metric: z21.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
12383
- 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)")
12384
12502
  }
12385
12503
  },
12386
- async ({ mode, metric, days_back }) => {
12504
+ async ({ mode, metric, days_back, limit: eventLimit }) => {
12387
12505
  const index = getIndex();
12388
12506
  const stateDb2 = getStateDb();
12389
12507
  const daysBack = days_back ?? 30;
@@ -12425,6 +12543,20 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
12425
12543
  };
12426
12544
  break;
12427
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
+ }
12428
12560
  }
12429
12561
  return {
12430
12562
  content: [
@@ -12708,7 +12840,7 @@ if (_originalRegisterTool) {
12708
12840
  }
12709
12841
  var categoryList = Array.from(enabledCategories).sort().join(", ");
12710
12842
  console.error(`[Memory] Tool categories: ${categoryList}`);
12711
- registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
12843
+ registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
12712
12844
  registerSystemTools(
12713
12845
  server,
12714
12846
  () => vaultIndex,
@@ -12785,6 +12917,13 @@ async function main() {
12785
12917
  setIndexState("ready");
12786
12918
  const duration = Date.now() - startTime;
12787
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
+ }
12788
12927
  runPostIndexWork(vaultIndex);
12789
12928
  } else {
12790
12929
  console.error("[Memory] Building vault index...");
@@ -12793,6 +12932,13 @@ async function main() {
12793
12932
  setIndexState("ready");
12794
12933
  const duration = Date.now() - startTime;
12795
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
+ }
12796
12942
  if (stateDb) {
12797
12943
  try {
12798
12944
  saveVaultIndexToCache(stateDb, vaultIndex);
@@ -12805,6 +12951,15 @@ async function main() {
12805
12951
  } catch (err) {
12806
12952
  setIndexState("error");
12807
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
+ }
12808
12963
  console.error("[Memory] Failed to build vault index:", err);
12809
12964
  }
12810
12965
  }
@@ -12849,6 +13004,7 @@ async function runPostIndexWork(index) {
12849
13004
  const metrics = computeMetrics(index, stateDb);
12850
13005
  recordMetrics(stateDb, metrics);
12851
13006
  purgeOldMetrics(stateDb, 90);
13007
+ purgeOldIndexEvents(stateDb, 90);
12852
13008
  console.error("[Memory] Growth metrics recorded");
12853
13009
  } catch (err) {
12854
13010
  console.error("[Memory] Failed to record metrics:", err);
@@ -12878,11 +13034,22 @@ async function runPostIndexWork(index) {
12878
13034
  config,
12879
13035
  onBatch: async (batch) => {
12880
13036
  console.error(`[Memory] Processing ${batch.events.length} file changes`);
12881
- const startTime = Date.now();
13037
+ const batchStart = Date.now();
13038
+ const changedPaths = batch.events.map((e) => e.path);
12882
13039
  try {
12883
13040
  vaultIndex = await buildVaultIndex(vaultPath);
12884
13041
  setIndexState("ready");
12885
- console.error(`[Memory] Index rebuilt in ${Date.now() - startTime}ms`);
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
+ }
12886
13053
  await updateEntitiesInStateDb();
12887
13054
  await exportHubScores(vaultIndex, stateDb);
12888
13055
  if (stateDb) {
@@ -12895,6 +13062,17 @@ async function runPostIndexWork(index) {
12895
13062
  } catch (err) {
12896
13063
  setIndexState("error");
12897
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
+ });
13075
+ }
12898
13076
  console.error("[Memory] Failed to rebuild index:", err);
12899
13077
  }
12900
13078
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.17",
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.17",
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",