@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.
- package/dist/index.js +188 -10
- 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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|