@velvetmonkey/flywheel-memory 2.0.29 → 2.0.31
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 +291 -116
- package/package.json +84 -84
package/dist/index.js
CHANGED
|
@@ -2485,7 +2485,8 @@ import {
|
|
|
2485
2485
|
} from "@velvetmonkey/vault-core";
|
|
2486
2486
|
var DEFAULT_CONFIG = {
|
|
2487
2487
|
exclude_task_tags: [],
|
|
2488
|
-
exclude_analysis_tags: []
|
|
2488
|
+
exclude_analysis_tags: [],
|
|
2489
|
+
exclude_entities: []
|
|
2489
2490
|
};
|
|
2490
2491
|
function loadConfig(stateDb2) {
|
|
2491
2492
|
if (stateDb2) {
|
|
@@ -5898,36 +5899,38 @@ async function buildFTS5Index(vaultPath2) {
|
|
|
5898
5899
|
if (!db2) {
|
|
5899
5900
|
throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
|
|
5900
5901
|
}
|
|
5901
|
-
db2.exec("DELETE FROM notes_fts");
|
|
5902
5902
|
const files = await scanVault(vaultPath2);
|
|
5903
5903
|
const indexableFiles = files.filter((f) => shouldIndexFile2(f.path));
|
|
5904
|
+
const rows = [];
|
|
5905
|
+
for (const file of indexableFiles) {
|
|
5906
|
+
try {
|
|
5907
|
+
const stats = fs7.statSync(file.absolutePath);
|
|
5908
|
+
if (stats.size > MAX_INDEX_FILE_SIZE) {
|
|
5909
|
+
continue;
|
|
5910
|
+
}
|
|
5911
|
+
const raw = fs7.readFileSync(file.absolutePath, "utf-8");
|
|
5912
|
+
const { frontmatter, body } = splitFrontmatter(raw);
|
|
5913
|
+
const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
|
|
5914
|
+
rows.push([file.path, title, frontmatter, body]);
|
|
5915
|
+
} catch (err) {
|
|
5916
|
+
console.error(`[FTS5] Skipping ${file.path}:`, err);
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5904
5919
|
const insert = db2.prepare(
|
|
5905
5920
|
"INSERT INTO notes_fts (path, title, frontmatter, content) VALUES (?, ?, ?, ?)"
|
|
5906
5921
|
);
|
|
5907
|
-
const insertMany = db2.transaction((filesToIndex) => {
|
|
5908
|
-
let indexed2 = 0;
|
|
5909
|
-
for (const file of filesToIndex) {
|
|
5910
|
-
try {
|
|
5911
|
-
const stats = fs7.statSync(file.absolutePath);
|
|
5912
|
-
if (stats.size > MAX_INDEX_FILE_SIZE) {
|
|
5913
|
-
continue;
|
|
5914
|
-
}
|
|
5915
|
-
const raw = fs7.readFileSync(file.absolutePath, "utf-8");
|
|
5916
|
-
const { frontmatter, body } = splitFrontmatter(raw);
|
|
5917
|
-
const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
|
|
5918
|
-
insert.run(file.path, title, frontmatter, body);
|
|
5919
|
-
indexed2++;
|
|
5920
|
-
} catch (err) {
|
|
5921
|
-
console.error(`[FTS5] Skipping ${file.path}:`, err);
|
|
5922
|
-
}
|
|
5923
|
-
}
|
|
5924
|
-
return indexed2;
|
|
5925
|
-
});
|
|
5926
|
-
const indexed = insertMany(indexableFiles);
|
|
5927
5922
|
const now = /* @__PURE__ */ new Date();
|
|
5928
|
-
db2.
|
|
5929
|
-
"
|
|
5930
|
-
|
|
5923
|
+
const swapAll = db2.transaction(() => {
|
|
5924
|
+
db2.exec("DELETE FROM notes_fts");
|
|
5925
|
+
for (const row of rows) {
|
|
5926
|
+
insert.run(...row);
|
|
5927
|
+
}
|
|
5928
|
+
db2.prepare(
|
|
5929
|
+
"INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
|
|
5930
|
+
).run("last_built", now.toISOString());
|
|
5931
|
+
});
|
|
5932
|
+
swapAll();
|
|
5933
|
+
const indexed = rows.length;
|
|
5931
5934
|
state = {
|
|
5932
5935
|
ready: true,
|
|
5933
5936
|
building: false,
|
|
@@ -6187,6 +6190,9 @@ function setTaskCacheDatabase(database) {
|
|
|
6187
6190
|
function isTaskCacheReady() {
|
|
6188
6191
|
return cacheReady && db3 !== null;
|
|
6189
6192
|
}
|
|
6193
|
+
function isTaskCacheBuilding() {
|
|
6194
|
+
return rebuildInProgress;
|
|
6195
|
+
}
|
|
6190
6196
|
async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
6191
6197
|
if (!db3) {
|
|
6192
6198
|
throw new Error("Task cache database not initialized. Call setTaskCacheDatabase() first.");
|
|
@@ -6195,53 +6201,47 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
6195
6201
|
rebuildInProgress = true;
|
|
6196
6202
|
const start = Date.now();
|
|
6197
6203
|
try {
|
|
6204
|
+
const notePaths = [];
|
|
6205
|
+
for (const note of index.notes.values()) {
|
|
6206
|
+
notePaths.push(note.path);
|
|
6207
|
+
}
|
|
6208
|
+
const allRows = [];
|
|
6209
|
+
for (const notePath of notePaths) {
|
|
6210
|
+
const absolutePath = path10.join(vaultPath2, notePath);
|
|
6211
|
+
const tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6212
|
+
for (const task of tasks) {
|
|
6213
|
+
if (excludeTags?.length && excludeTags.some((t) => task.tags.includes(t))) {
|
|
6214
|
+
continue;
|
|
6215
|
+
}
|
|
6216
|
+
allRows.push([
|
|
6217
|
+
task.path,
|
|
6218
|
+
task.line,
|
|
6219
|
+
task.text,
|
|
6220
|
+
task.status,
|
|
6221
|
+
task.raw,
|
|
6222
|
+
task.context ?? null,
|
|
6223
|
+
task.tags.length > 0 ? JSON.stringify(task.tags) : null,
|
|
6224
|
+
task.due_date ?? null
|
|
6225
|
+
]);
|
|
6226
|
+
}
|
|
6227
|
+
}
|
|
6198
6228
|
const insertStmt = db3.prepare(`
|
|
6199
6229
|
INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
|
|
6200
6230
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6201
6231
|
`);
|
|
6202
|
-
const
|
|
6232
|
+
const swapAll = db3.transaction(() => {
|
|
6203
6233
|
db3.prepare("DELETE FROM tasks").run();
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
const notePaths2 = [];
|
|
6207
|
-
for (const note of index.notes.values()) {
|
|
6208
|
-
notePaths2.push(note.path);
|
|
6234
|
+
for (const row of allRows) {
|
|
6235
|
+
insertStmt.run(...row);
|
|
6209
6236
|
}
|
|
6210
|
-
|
|
6237
|
+
db3.prepare(
|
|
6238
|
+
"INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
|
|
6239
|
+
).run("task_cache_built", (/* @__PURE__ */ new Date()).toISOString());
|
|
6211
6240
|
});
|
|
6212
|
-
|
|
6213
|
-
let totalTasks = 0;
|
|
6214
|
-
for (const notePath of notePaths) {
|
|
6215
|
-
const absolutePath = path10.join(vaultPath2, notePath);
|
|
6216
|
-
const tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6217
|
-
if (tasks.length > 0) {
|
|
6218
|
-
const insertBatch = db3.transaction(() => {
|
|
6219
|
-
for (const task of tasks) {
|
|
6220
|
-
if (excludeTags?.length && excludeTags.some((t) => task.tags.includes(t))) {
|
|
6221
|
-
continue;
|
|
6222
|
-
}
|
|
6223
|
-
stmt.run(
|
|
6224
|
-
task.path,
|
|
6225
|
-
task.line,
|
|
6226
|
-
task.text,
|
|
6227
|
-
task.status,
|
|
6228
|
-
task.raw,
|
|
6229
|
-
task.context ?? null,
|
|
6230
|
-
task.tags.length > 0 ? JSON.stringify(task.tags) : null,
|
|
6231
|
-
task.due_date ?? null
|
|
6232
|
-
);
|
|
6233
|
-
totalTasks++;
|
|
6234
|
-
}
|
|
6235
|
-
});
|
|
6236
|
-
insertBatch();
|
|
6237
|
-
}
|
|
6238
|
-
}
|
|
6239
|
-
db3.prepare(
|
|
6240
|
-
"INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
|
|
6241
|
-
).run("task_cache_built", (/* @__PURE__ */ new Date()).toISOString());
|
|
6241
|
+
swapAll();
|
|
6242
6242
|
cacheReady = true;
|
|
6243
6243
|
const duration = Date.now() - start;
|
|
6244
|
-
serverLog("tasks", `Task cache built: ${
|
|
6244
|
+
serverLog("tasks", `Task cache built: ${allRows.length} tasks from ${notePaths.length} notes in ${duration}ms`);
|
|
6245
6245
|
} finally {
|
|
6246
6246
|
rebuildInProgress = false;
|
|
6247
6247
|
}
|
|
@@ -6675,10 +6675,20 @@ async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
|
|
|
6675
6675
|
try {
|
|
6676
6676
|
const fullPath = path11.join(vaultPath2, sourcePath);
|
|
6677
6677
|
const content = await fs9.promises.readFile(fullPath, "utf-8");
|
|
6678
|
-
const
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6678
|
+
const allLines = content.split("\n");
|
|
6679
|
+
let fmLines = 0;
|
|
6680
|
+
if (allLines[0]?.trimEnd() === "---") {
|
|
6681
|
+
for (let i = 1; i < allLines.length; i++) {
|
|
6682
|
+
if (allLines[i].trimEnd() === "---") {
|
|
6683
|
+
fmLines = i + 1;
|
|
6684
|
+
break;
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
const absLine = line + fmLines;
|
|
6689
|
+
const startLine = Math.max(0, absLine - 1 - contextLines);
|
|
6690
|
+
const endLine = Math.min(allLines.length, absLine + contextLines);
|
|
6691
|
+
return allLines.slice(startLine, endLine).join("\n").trim();
|
|
6682
6692
|
} catch {
|
|
6683
6693
|
return "";
|
|
6684
6694
|
}
|
|
@@ -7303,10 +7313,28 @@ function getActivitySummary(index, days) {
|
|
|
7303
7313
|
import { SCHEMA_VERSION } from "@velvetmonkey/vault-core";
|
|
7304
7314
|
|
|
7305
7315
|
// src/core/shared/indexActivity.ts
|
|
7316
|
+
function createStepTracker() {
|
|
7317
|
+
const steps = [];
|
|
7318
|
+
let current = null;
|
|
7319
|
+
return {
|
|
7320
|
+
steps,
|
|
7321
|
+
start(name, input) {
|
|
7322
|
+
current = { name, input, startTime: Date.now() };
|
|
7323
|
+
},
|
|
7324
|
+
end(output) {
|
|
7325
|
+
if (!current) return;
|
|
7326
|
+
steps.push({ name: current.name, duration_ms: Date.now() - current.startTime, input: current.input, output });
|
|
7327
|
+
current = null;
|
|
7328
|
+
},
|
|
7329
|
+
skip(name, reason) {
|
|
7330
|
+
steps.push({ name, duration_ms: 0, input: {}, output: {}, skipped: true, skip_reason: reason });
|
|
7331
|
+
}
|
|
7332
|
+
};
|
|
7333
|
+
}
|
|
7306
7334
|
function recordIndexEvent(stateDb2, event) {
|
|
7307
7335
|
stateDb2.db.prepare(
|
|
7308
|
-
`INSERT INTO index_events (timestamp, trigger, duration_ms, success, note_count, files_changed, changed_paths, error)
|
|
7309
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7336
|
+
`INSERT INTO index_events (timestamp, trigger, duration_ms, success, note_count, files_changed, changed_paths, error, steps)
|
|
7337
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7310
7338
|
).run(
|
|
7311
7339
|
Date.now(),
|
|
7312
7340
|
event.trigger,
|
|
@@ -7315,7 +7343,8 @@ function recordIndexEvent(stateDb2, event) {
|
|
|
7315
7343
|
event.note_count ?? null,
|
|
7316
7344
|
event.files_changed ?? null,
|
|
7317
7345
|
event.changed_paths ? JSON.stringify(event.changed_paths) : null,
|
|
7318
|
-
event.error ?? null
|
|
7346
|
+
event.error ?? null,
|
|
7347
|
+
event.steps ? JSON.stringify(event.steps) : null
|
|
7319
7348
|
);
|
|
7320
7349
|
}
|
|
7321
7350
|
function rowToEvent(row) {
|
|
@@ -7328,9 +7357,41 @@ function rowToEvent(row) {
|
|
|
7328
7357
|
note_count: row.note_count,
|
|
7329
7358
|
files_changed: row.files_changed,
|
|
7330
7359
|
changed_paths: row.changed_paths ? JSON.parse(row.changed_paths) : null,
|
|
7331
|
-
error: row.error
|
|
7360
|
+
error: row.error,
|
|
7361
|
+
steps: row.steps ? JSON.parse(row.steps) : null
|
|
7332
7362
|
};
|
|
7333
7363
|
}
|
|
7364
|
+
function getRecentPipelineEvent(stateDb2) {
|
|
7365
|
+
const row = stateDb2.db.prepare(
|
|
7366
|
+
"SELECT * FROM index_events WHERE steps IS NOT NULL ORDER BY timestamp DESC LIMIT 1"
|
|
7367
|
+
).get();
|
|
7368
|
+
return row ? rowToEvent(row) : null;
|
|
7369
|
+
}
|
|
7370
|
+
function computeEntityDiff(before, after) {
|
|
7371
|
+
const beforeMap = new Map(before.map((e) => [e.nameLower, e]));
|
|
7372
|
+
const afterMap = new Map(after.map((e) => [e.nameLower, e]));
|
|
7373
|
+
const added = [];
|
|
7374
|
+
const removed = [];
|
|
7375
|
+
const alias_changes = [];
|
|
7376
|
+
for (const [key, entity] of afterMap) {
|
|
7377
|
+
if (!beforeMap.has(key)) {
|
|
7378
|
+
added.push(entity.name);
|
|
7379
|
+
} else {
|
|
7380
|
+
const prev = beforeMap.get(key);
|
|
7381
|
+
const prevAliases = JSON.stringify(prev.aliases.sort());
|
|
7382
|
+
const currAliases = JSON.stringify(entity.aliases.sort());
|
|
7383
|
+
if (prevAliases !== currAliases) {
|
|
7384
|
+
alias_changes.push({ entity: entity.name, before: prev.aliases, after: entity.aliases });
|
|
7385
|
+
}
|
|
7386
|
+
}
|
|
7387
|
+
}
|
|
7388
|
+
for (const [key, entity] of beforeMap) {
|
|
7389
|
+
if (!afterMap.has(key)) {
|
|
7390
|
+
removed.push(entity.name);
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
return { added, removed, alias_changes };
|
|
7394
|
+
}
|
|
7334
7395
|
function getRecentIndexEvents(stateDb2, limit = 20) {
|
|
7335
7396
|
const rows = stateDb2.db.prepare(
|
|
7336
7397
|
"SELECT * FROM index_events ORDER BY timestamp DESC LIMIT ?"
|
|
@@ -7415,11 +7476,27 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7415
7476
|
duration_ms: z3.number(),
|
|
7416
7477
|
ago_seconds: z3.number()
|
|
7417
7478
|
}).optional().describe("Most recent index rebuild event"),
|
|
7479
|
+
last_pipeline: z3.object({
|
|
7480
|
+
timestamp: z3.number(),
|
|
7481
|
+
trigger: z3.string(),
|
|
7482
|
+
duration_ms: z3.number(),
|
|
7483
|
+
files_changed: z3.number().nullable(),
|
|
7484
|
+
steps: z3.array(z3.object({
|
|
7485
|
+
name: z3.string(),
|
|
7486
|
+
duration_ms: z3.number(),
|
|
7487
|
+
input: z3.record(z3.unknown()),
|
|
7488
|
+
output: z3.record(z3.unknown()),
|
|
7489
|
+
skipped: z3.boolean().optional(),
|
|
7490
|
+
skip_reason: z3.string().optional()
|
|
7491
|
+
}))
|
|
7492
|
+
}).optional().describe("Most recent watcher pipeline run with per-step timing"),
|
|
7418
7493
|
fts5_ready: z3.boolean().describe("Whether the FTS5 keyword search index is ready"),
|
|
7419
7494
|
fts5_building: z3.boolean().describe("Whether the FTS5 keyword search index is currently building"),
|
|
7420
7495
|
embeddings_building: z3.boolean().describe("Whether semantic embeddings are currently building"),
|
|
7421
7496
|
embeddings_ready: z3.boolean().describe("Whether semantic embeddings have been built (enables hybrid keyword+semantic search)"),
|
|
7422
7497
|
embeddings_count: z3.coerce.number().describe("Number of notes with semantic embeddings"),
|
|
7498
|
+
tasks_ready: z3.boolean().describe("Whether the task cache is ready to serve queries"),
|
|
7499
|
+
tasks_building: z3.boolean().describe("Whether the task cache is currently rebuilding"),
|
|
7423
7500
|
recommendations: z3.array(z3.string()).describe("Suggested actions if any issues detected")
|
|
7424
7501
|
};
|
|
7425
7502
|
server2.registerTool(
|
|
@@ -7458,7 +7535,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7458
7535
|
recommendations.push(`Index is ${Math.floor(indexAge / 60)} minutes old. Consider running refresh_index.`);
|
|
7459
7536
|
}
|
|
7460
7537
|
const noteCount = indexBuilt ? index.notes.size : 0;
|
|
7461
|
-
const
|
|
7538
|
+
const entityCount2 = indexBuilt ? index.entities.size : 0;
|
|
7462
7539
|
const tagCount = indexBuilt ? index.tags.size : 0;
|
|
7463
7540
|
let linkCount = 0;
|
|
7464
7541
|
if (indexBuilt) {
|
|
@@ -7509,6 +7586,22 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7509
7586
|
} catch {
|
|
7510
7587
|
}
|
|
7511
7588
|
}
|
|
7589
|
+
let lastPipeline;
|
|
7590
|
+
if (stateDb2) {
|
|
7591
|
+
try {
|
|
7592
|
+
const evt = getRecentPipelineEvent(stateDb2);
|
|
7593
|
+
if (evt && evt.steps && evt.steps.length > 0) {
|
|
7594
|
+
lastPipeline = {
|
|
7595
|
+
timestamp: evt.timestamp,
|
|
7596
|
+
trigger: evt.trigger,
|
|
7597
|
+
duration_ms: evt.duration_ms,
|
|
7598
|
+
files_changed: evt.files_changed,
|
|
7599
|
+
steps: evt.steps
|
|
7600
|
+
};
|
|
7601
|
+
}
|
|
7602
|
+
} catch {
|
|
7603
|
+
}
|
|
7604
|
+
}
|
|
7512
7605
|
const ftsState = getFTS5State();
|
|
7513
7606
|
const output = {
|
|
7514
7607
|
status,
|
|
@@ -7522,17 +7615,20 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7522
7615
|
index_age_seconds: indexAge,
|
|
7523
7616
|
index_stale: indexStale,
|
|
7524
7617
|
note_count: noteCount,
|
|
7525
|
-
entity_count:
|
|
7618
|
+
entity_count: entityCount2,
|
|
7526
7619
|
tag_count: tagCount,
|
|
7527
7620
|
link_count: linkCount,
|
|
7528
7621
|
periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
|
|
7529
7622
|
config: configInfo,
|
|
7530
7623
|
last_rebuild: lastRebuild,
|
|
7624
|
+
last_pipeline: lastPipeline,
|
|
7531
7625
|
fts5_ready: ftsState.ready,
|
|
7532
7626
|
fts5_building: ftsState.building,
|
|
7533
7627
|
embeddings_building: isEmbeddingsBuilding(),
|
|
7534
7628
|
embeddings_ready: hasEmbeddingsIndex(),
|
|
7535
7629
|
embeddings_count: getEmbeddingsCount(),
|
|
7630
|
+
tasks_ready: isTaskCacheReady(),
|
|
7631
|
+
tasks_building: isTaskCacheBuilding(),
|
|
7536
7632
|
recommendations
|
|
7537
7633
|
};
|
|
7538
7634
|
return {
|
|
@@ -9787,6 +9883,35 @@ function isPeriodicNote(notePath) {
|
|
|
9787
9883
|
const folder = notePath.split("/")[0]?.toLowerCase() || "";
|
|
9788
9884
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
9789
9885
|
}
|
|
9886
|
+
function getExcludedPaths(index, config) {
|
|
9887
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
9888
|
+
const excludeTags = new Set((config.exclude_analysis_tags ?? []).map((t) => t.toLowerCase()));
|
|
9889
|
+
const excludeEntities = new Set((config.exclude_entities ?? []).map((e) => e.toLowerCase()));
|
|
9890
|
+
if (excludeTags.size === 0 && excludeEntities.size === 0) return excluded;
|
|
9891
|
+
for (const note of index.notes.values()) {
|
|
9892
|
+
if (excludeTags.size > 0) {
|
|
9893
|
+
const tags = note.frontmatter?.tags;
|
|
9894
|
+
const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
|
|
9895
|
+
if (tagList.some((t) => excludeTags.has(String(t).toLowerCase()))) {
|
|
9896
|
+
excluded.add(note.path);
|
|
9897
|
+
continue;
|
|
9898
|
+
}
|
|
9899
|
+
}
|
|
9900
|
+
if (excludeEntities.size > 0) {
|
|
9901
|
+
if (excludeEntities.has(note.title.toLowerCase())) {
|
|
9902
|
+
excluded.add(note.path);
|
|
9903
|
+
continue;
|
|
9904
|
+
}
|
|
9905
|
+
for (const alias of note.aliases) {
|
|
9906
|
+
if (excludeEntities.has(alias.toLowerCase())) {
|
|
9907
|
+
excluded.add(note.path);
|
|
9908
|
+
break;
|
|
9909
|
+
}
|
|
9910
|
+
}
|
|
9911
|
+
}
|
|
9912
|
+
}
|
|
9913
|
+
return excluded;
|
|
9914
|
+
}
|
|
9790
9915
|
function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb, getConfig) {
|
|
9791
9916
|
server2.registerTool(
|
|
9792
9917
|
"graph_analysis",
|
|
@@ -9808,9 +9933,11 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9808
9933
|
requireIndex();
|
|
9809
9934
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
9810
9935
|
const index = getIndex();
|
|
9936
|
+
const config = getConfig?.() ?? {};
|
|
9937
|
+
const excludedPaths = getExcludedPaths(index, config);
|
|
9811
9938
|
switch (analysis) {
|
|
9812
9939
|
case "orphans": {
|
|
9813
|
-
const allOrphans = findOrphanNotes(index, folder).filter((o) => !isPeriodicNote(o.path));
|
|
9940
|
+
const allOrphans = findOrphanNotes(index, folder).filter((o) => !isPeriodicNote(o.path) && !excludedPaths.has(o.path));
|
|
9814
9941
|
const orphans = allOrphans.slice(offset, offset + limit);
|
|
9815
9942
|
return {
|
|
9816
9943
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -9827,7 +9954,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9827
9954
|
};
|
|
9828
9955
|
}
|
|
9829
9956
|
case "dead_ends": {
|
|
9830
|
-
const allResults = findDeadEnds(index, folder, min_backlinks);
|
|
9957
|
+
const allResults = findDeadEnds(index, folder, min_backlinks).filter((n) => !excludedPaths.has(n.path));
|
|
9831
9958
|
const result = allResults.slice(offset, offset + limit);
|
|
9832
9959
|
return {
|
|
9833
9960
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -9840,7 +9967,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9840
9967
|
};
|
|
9841
9968
|
}
|
|
9842
9969
|
case "sources": {
|
|
9843
|
-
const allResults = findSources(index, folder, min_outlinks);
|
|
9970
|
+
const allResults = findSources(index, folder, min_outlinks).filter((n) => !excludedPaths.has(n.path));
|
|
9844
9971
|
const result = allResults.slice(offset, offset + limit);
|
|
9845
9972
|
return {
|
|
9846
9973
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -9853,17 +9980,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9853
9980
|
};
|
|
9854
9981
|
}
|
|
9855
9982
|
case "hubs": {
|
|
9856
|
-
const
|
|
9857
|
-
(getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
|
|
9858
|
-
);
|
|
9859
|
-
const allHubs = findHubNotes(index, min_links).filter((h) => {
|
|
9860
|
-
if (excludeTags.size === 0) return true;
|
|
9861
|
-
const note = index.notes.get(h.path);
|
|
9862
|
-
if (!note) return true;
|
|
9863
|
-
const tags = note.frontmatter?.tags;
|
|
9864
|
-
const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
|
|
9865
|
-
return !tagList.some((t) => excludeTags.has(String(t).toLowerCase()));
|
|
9866
|
-
});
|
|
9983
|
+
const allHubs = findHubNotes(index, min_links).filter((h) => !excludedPaths.has(h.path));
|
|
9867
9984
|
const hubs = allHubs.slice(offset, offset + limit);
|
|
9868
9985
|
return {
|
|
9869
9986
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -9889,7 +10006,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9889
10006
|
}, null, 2) }]
|
|
9890
10007
|
};
|
|
9891
10008
|
}
|
|
9892
|
-
const result = getStaleNotes(index, days, min_backlinks).slice(0, limit);
|
|
10009
|
+
const result = getStaleNotes(index, days, min_backlinks).filter((n) => !excludedPaths.has(n.path)).slice(0, limit);
|
|
9893
10010
|
return {
|
|
9894
10011
|
content: [{ type: "text", text: JSON.stringify({
|
|
9895
10012
|
analysis: "stale",
|
|
@@ -9905,7 +10022,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9905
10022
|
case "immature": {
|
|
9906
10023
|
const vaultPath2 = getVaultPath();
|
|
9907
10024
|
const allNotes = Array.from(index.notes.values()).filter(
|
|
9908
|
-
(note) => (!folder || note.path.startsWith(folder + "/") || note.path.substring(0, note.path.lastIndexOf("/")) === folder) && !isPeriodicNote(note.path)
|
|
10025
|
+
(note) => (!folder || note.path.startsWith(folder + "/") || note.path.substring(0, note.path.lastIndexOf("/")) === folder) && !isPeriodicNote(note.path) && !excludedPaths.has(note.path)
|
|
9909
10026
|
);
|
|
9910
10027
|
const conventions = inferFolderConventions(index, folder, 0.5);
|
|
9911
10028
|
const expectedFields = conventions.inferred_fields.map((f) => f.name);
|
|
@@ -9990,22 +10107,20 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9990
10107
|
}
|
|
9991
10108
|
const daysBack = days ?? 30;
|
|
9992
10109
|
let hubs = getEmergingHubs(db4, daysBack);
|
|
9993
|
-
|
|
9994
|
-
(getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
|
|
9995
|
-
);
|
|
9996
|
-
if (excludeTags.size > 0) {
|
|
10110
|
+
if (excludedPaths.size > 0) {
|
|
9997
10111
|
const notesByTitle = /* @__PURE__ */ new Map();
|
|
9998
10112
|
for (const note of index.notes.values()) {
|
|
9999
10113
|
notesByTitle.set(note.title.toLowerCase(), note);
|
|
10000
10114
|
}
|
|
10001
10115
|
hubs = hubs.filter((hub) => {
|
|
10002
10116
|
const note = notesByTitle.get(hub.entity.toLowerCase());
|
|
10003
|
-
|
|
10004
|
-
const tags = note.frontmatter?.tags;
|
|
10005
|
-
const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
|
|
10006
|
-
return !tagList.some((t) => excludeTags.has(String(t).toLowerCase()));
|
|
10117
|
+
return !note || !excludedPaths.has(note.path);
|
|
10007
10118
|
});
|
|
10008
10119
|
}
|
|
10120
|
+
const excludeEntities = new Set((config.exclude_entities ?? []).map((e) => e.toLowerCase()));
|
|
10121
|
+
if (excludeEntities.size > 0) {
|
|
10122
|
+
hubs = hubs.filter((hub) => !excludeEntities.has(hub.entity.toLowerCase()));
|
|
10123
|
+
}
|
|
10009
10124
|
return {
|
|
10010
10125
|
content: [{ type: "text", text: JSON.stringify({
|
|
10011
10126
|
analysis: "emerging_hubs",
|
|
@@ -14429,7 +14544,7 @@ function computeMetrics(index, stateDb2) {
|
|
|
14429
14544
|
}
|
|
14430
14545
|
}
|
|
14431
14546
|
const tagCount = index.tags.size;
|
|
14432
|
-
const
|
|
14547
|
+
const entityCount2 = index.entities.size;
|
|
14433
14548
|
const avgLinksPerNote = noteCount > 0 ? linkCount / noteCount : 0;
|
|
14434
14549
|
const possibleLinks = noteCount * (noteCount - 1);
|
|
14435
14550
|
const linkDensity = possibleLinks > 0 ? linkCount / possibleLinks : 0;
|
|
@@ -14451,7 +14566,7 @@ function computeMetrics(index, stateDb2) {
|
|
|
14451
14566
|
link_count: linkCount,
|
|
14452
14567
|
orphan_count: orphanCount,
|
|
14453
14568
|
tag_count: tagCount,
|
|
14454
|
-
entity_count:
|
|
14569
|
+
entity_count: entityCount2,
|
|
14455
14570
|
avg_links_per_note: Math.round(avgLinksPerNote * 100) / 100,
|
|
14456
14571
|
link_density: Math.round(linkDensity * 1e4) / 1e4,
|
|
14457
14572
|
connected_ratio: Math.round(connectedRatio * 1e3) / 1e3,
|
|
@@ -15782,6 +15897,7 @@ async function main() {
|
|
|
15782
15897
|
setFTS5Database(stateDb.db);
|
|
15783
15898
|
setEmbeddingsDatabase(stateDb.db);
|
|
15784
15899
|
setTaskCacheDatabase(stateDb.db);
|
|
15900
|
+
serverLog("statedb", "Injected FTS5, embeddings, task cache handles");
|
|
15785
15901
|
loadEntityEmbeddingsToMemory();
|
|
15786
15902
|
setWriteStateDb(stateDb);
|
|
15787
15903
|
} catch (err) {
|
|
@@ -15830,7 +15946,8 @@ async function main() {
|
|
|
15830
15946
|
vaultIndex = cachedIndex;
|
|
15831
15947
|
setIndexState("ready");
|
|
15832
15948
|
const duration = Date.now() - startTime;
|
|
15833
|
-
|
|
15949
|
+
const cacheAge = cachedIndex.builtAt ? Math.round((Date.now() - cachedIndex.builtAt.getTime()) / 1e3) : 0;
|
|
15950
|
+
serverLog("index", `Cache hit: ${cachedIndex.notes.size} notes, ${cacheAge}s old \u2014 loaded in ${duration}ms`);
|
|
15834
15951
|
if (stateDb) {
|
|
15835
15952
|
recordIndexEvent(stateDb, {
|
|
15836
15953
|
trigger: "startup_cache",
|
|
@@ -15840,7 +15957,7 @@ async function main() {
|
|
|
15840
15957
|
}
|
|
15841
15958
|
runPostIndexWork(vaultIndex);
|
|
15842
15959
|
} else {
|
|
15843
|
-
serverLog("index", "
|
|
15960
|
+
serverLog("index", "Cache miss: building from scratch");
|
|
15844
15961
|
try {
|
|
15845
15962
|
vaultIndex = await buildVaultIndex(vaultPath);
|
|
15846
15963
|
setIndexState("ready");
|
|
@@ -15911,9 +16028,13 @@ async function updateEntitiesInStateDb() {
|
|
|
15911
16028
|
}
|
|
15912
16029
|
}
|
|
15913
16030
|
async function runPostIndexWork(index) {
|
|
16031
|
+
const postStart = Date.now();
|
|
16032
|
+
serverLog("index", "Scanning entities...");
|
|
15914
16033
|
await updateEntitiesInStateDb();
|
|
15915
16034
|
await initializeEntityIndex(vaultPath);
|
|
16035
|
+
serverLog("index", "Entity index initialized");
|
|
15916
16036
|
await exportHubScores(index, stateDb);
|
|
16037
|
+
serverLog("index", "Hub scores exported");
|
|
15917
16038
|
if (stateDb) {
|
|
15918
16039
|
try {
|
|
15919
16040
|
const metrics = computeMetrics(index, stateDb);
|
|
@@ -15938,6 +16059,7 @@ async function runPostIndexWork(index) {
|
|
|
15938
16059
|
if (stateDb) {
|
|
15939
16060
|
try {
|
|
15940
16061
|
updateSuppressionList(stateDb);
|
|
16062
|
+
serverLog("index", "Suppression list updated");
|
|
15941
16063
|
} catch (err) {
|
|
15942
16064
|
serverLog("server", `Failed to update suppression list: ${err instanceof Error ? err.message : err}`, "error");
|
|
15943
16065
|
}
|
|
@@ -15948,9 +16070,15 @@ async function runPostIndexWork(index) {
|
|
|
15948
16070
|
saveConfig(stateDb, inferred, existing);
|
|
15949
16071
|
}
|
|
15950
16072
|
flywheelConfig = loadConfig(stateDb);
|
|
16073
|
+
const configKeys = Object.keys(flywheelConfig).filter((k) => flywheelConfig[k] != null);
|
|
16074
|
+
serverLog("config", `Config inferred: ${configKeys.join(", ")}`);
|
|
15951
16075
|
if (stateDb) {
|
|
15952
|
-
|
|
15953
|
-
|
|
16076
|
+
if (isTaskCacheStale()) {
|
|
16077
|
+
serverLog("tasks", "Task cache stale, rebuilding...");
|
|
16078
|
+
refreshIfStale(vaultPath, index, flywheelConfig.exclude_task_tags);
|
|
16079
|
+
} else {
|
|
16080
|
+
serverLog("tasks", "Task cache fresh, skipping rebuild");
|
|
16081
|
+
}
|
|
15954
16082
|
}
|
|
15955
16083
|
if (flywheelConfig.vault_name) {
|
|
15956
16084
|
serverLog("config", `Vault: ${flywheelConfig.vault_name}`);
|
|
@@ -15996,36 +16124,49 @@ async function runPostIndexWork(index) {
|
|
|
15996
16124
|
serverLog("watcher", `Processing ${batch.events.length} file changes`);
|
|
15997
16125
|
const batchStart = Date.now();
|
|
15998
16126
|
const changedPaths = batch.events.map((e) => e.path);
|
|
16127
|
+
const tracker = createStepTracker();
|
|
15999
16128
|
try {
|
|
16129
|
+
tracker.start("index_rebuild", { files_changed: batch.events.length, changed_paths: changedPaths });
|
|
16000
16130
|
vaultIndex = await buildVaultIndex(vaultPath);
|
|
16001
16131
|
setIndexState("ready");
|
|
16002
|
-
|
|
16003
|
-
serverLog("watcher", `Index rebuilt
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
trigger: "watcher",
|
|
16007
|
-
duration_ms: duration,
|
|
16008
|
-
note_count: vaultIndex.notes.size,
|
|
16009
|
-
files_changed: batch.events.length,
|
|
16010
|
-
changed_paths: changedPaths
|
|
16011
|
-
});
|
|
16012
|
-
}
|
|
16132
|
+
tracker.end({ note_count: vaultIndex.notes.size, entity_count: vaultIndex.entities.size, tag_count: vaultIndex.tags.size });
|
|
16133
|
+
serverLog("watcher", `Index rebuilt: ${vaultIndex.notes.size} notes, ${vaultIndex.entities.size} entities`);
|
|
16134
|
+
const entitiesBefore = stateDb ? getAllEntitiesFromDb3(stateDb) : [];
|
|
16135
|
+
tracker.start("entity_scan", { note_count: vaultIndex.notes.size });
|
|
16013
16136
|
await updateEntitiesInStateDb();
|
|
16014
|
-
|
|
16137
|
+
const entitiesAfter = stateDb ? getAllEntitiesFromDb3(stateDb) : [];
|
|
16138
|
+
const entityDiff = computeEntityDiff(entitiesBefore, entitiesAfter);
|
|
16139
|
+
tracker.end({ entity_count: entitiesAfter.length, ...entityDiff });
|
|
16140
|
+
serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
|
|
16141
|
+
tracker.start("hub_scores", { entity_count: entityCount });
|
|
16142
|
+
const hubUpdated = await exportHubScores(vaultIndex, stateDb);
|
|
16143
|
+
tracker.end({ updated: hubUpdated ?? 0 });
|
|
16144
|
+
serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
|
|
16015
16145
|
if (hasEmbeddingsIndex()) {
|
|
16146
|
+
tracker.start("note_embeddings", { files: batch.events.length });
|
|
16147
|
+
let embUpdated = 0;
|
|
16148
|
+
let embRemoved = 0;
|
|
16016
16149
|
for (const event of batch.events) {
|
|
16017
16150
|
try {
|
|
16018
16151
|
if (event.type === "delete") {
|
|
16019
16152
|
removeEmbedding(event.path);
|
|
16153
|
+
embRemoved++;
|
|
16020
16154
|
} else if (event.path.endsWith(".md")) {
|
|
16021
16155
|
const absPath = path29.join(vaultPath, event.path);
|
|
16022
16156
|
await updateEmbedding(event.path, absPath);
|
|
16157
|
+
embUpdated++;
|
|
16023
16158
|
}
|
|
16024
16159
|
} catch {
|
|
16025
16160
|
}
|
|
16026
16161
|
}
|
|
16162
|
+
tracker.end({ updated: embUpdated, removed: embRemoved });
|
|
16163
|
+
serverLog("watcher", `Note embeddings: ${embUpdated} updated, ${embRemoved} removed`);
|
|
16164
|
+
} else {
|
|
16165
|
+
tracker.skip("note_embeddings", "not built");
|
|
16027
16166
|
}
|
|
16028
16167
|
if (hasEntityEmbeddingsIndex() && stateDb) {
|
|
16168
|
+
tracker.start("entity_embeddings", { files: batch.events.length });
|
|
16169
|
+
let entEmbUpdated = 0;
|
|
16029
16170
|
try {
|
|
16030
16171
|
const allEntities = getAllEntitiesFromDb3(stateDb);
|
|
16031
16172
|
for (const event of batch.events) {
|
|
@@ -16038,28 +16179,58 @@ async function runPostIndexWork(index) {
|
|
|
16038
16179
|
category: entity.category,
|
|
16039
16180
|
aliases: entity.aliases
|
|
16040
16181
|
}, vaultPath);
|
|
16182
|
+
entEmbUpdated++;
|
|
16041
16183
|
}
|
|
16042
16184
|
}
|
|
16043
16185
|
} catch {
|
|
16044
16186
|
}
|
|
16187
|
+
tracker.end({ updated: entEmbUpdated });
|
|
16188
|
+
serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated`);
|
|
16189
|
+
} else {
|
|
16190
|
+
tracker.skip("entity_embeddings", !stateDb ? "no stateDb" : "not built");
|
|
16045
16191
|
}
|
|
16046
16192
|
if (stateDb) {
|
|
16193
|
+
tracker.start("index_cache", { note_count: vaultIndex.notes.size });
|
|
16047
16194
|
try {
|
|
16048
16195
|
saveVaultIndexToCache(stateDb, vaultIndex);
|
|
16196
|
+
tracker.end({ saved: true });
|
|
16197
|
+
serverLog("watcher", "Index cache saved");
|
|
16049
16198
|
} catch (err) {
|
|
16199
|
+
tracker.end({ saved: false, error: err instanceof Error ? err.message : String(err) });
|
|
16050
16200
|
serverLog("index", `Failed to update index cache: ${err instanceof Error ? err.message : err}`, "error");
|
|
16051
16201
|
}
|
|
16202
|
+
} else {
|
|
16203
|
+
tracker.skip("index_cache", "no stateDb");
|
|
16052
16204
|
}
|
|
16205
|
+
tracker.start("task_cache", { files: batch.events.length });
|
|
16206
|
+
let taskUpdated = 0;
|
|
16207
|
+
let taskRemoved = 0;
|
|
16053
16208
|
for (const event of batch.events) {
|
|
16054
16209
|
try {
|
|
16055
16210
|
if (event.type === "delete") {
|
|
16056
16211
|
removeTaskCacheForFile(event.path);
|
|
16212
|
+
taskRemoved++;
|
|
16057
16213
|
} else if (event.path.endsWith(".md")) {
|
|
16058
16214
|
await updateTaskCacheForFile(vaultPath, event.path);
|
|
16215
|
+
taskUpdated++;
|
|
16059
16216
|
}
|
|
16060
16217
|
} catch {
|
|
16061
16218
|
}
|
|
16062
16219
|
}
|
|
16220
|
+
tracker.end({ updated: taskUpdated, removed: taskRemoved });
|
|
16221
|
+
serverLog("watcher", `Task cache: ${taskUpdated} updated, ${taskRemoved} removed`);
|
|
16222
|
+
const duration = Date.now() - batchStart;
|
|
16223
|
+
if (stateDb) {
|
|
16224
|
+
recordIndexEvent(stateDb, {
|
|
16225
|
+
trigger: "watcher",
|
|
16226
|
+
duration_ms: duration,
|
|
16227
|
+
note_count: vaultIndex.notes.size,
|
|
16228
|
+
files_changed: batch.events.length,
|
|
16229
|
+
changed_paths: changedPaths,
|
|
16230
|
+
steps: tracker.steps
|
|
16231
|
+
});
|
|
16232
|
+
}
|
|
16233
|
+
serverLog("watcher", `Batch complete: ${batch.events.length} files, ${duration}ms, ${tracker.steps.length} steps`);
|
|
16063
16234
|
} catch (err) {
|
|
16064
16235
|
setIndexState("error");
|
|
16065
16236
|
setIndexError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -16071,7 +16242,8 @@ async function runPostIndexWork(index) {
|
|
|
16071
16242
|
success: false,
|
|
16072
16243
|
files_changed: batch.events.length,
|
|
16073
16244
|
changed_paths: changedPaths,
|
|
16074
|
-
error: err instanceof Error ? err.message : String(err)
|
|
16245
|
+
error: err instanceof Error ? err.message : String(err),
|
|
16246
|
+
steps: tracker.steps
|
|
16075
16247
|
});
|
|
16076
16248
|
}
|
|
16077
16249
|
serverLog("watcher", `Failed to rebuild index: ${err instanceof Error ? err.message : err}`, "error");
|
|
@@ -16087,7 +16259,10 @@ async function runPostIndexWork(index) {
|
|
|
16087
16259
|
}
|
|
16088
16260
|
});
|
|
16089
16261
|
watcher.start();
|
|
16262
|
+
serverLog("watcher", "File watcher started");
|
|
16090
16263
|
}
|
|
16264
|
+
const postDuration = Date.now() - postStart;
|
|
16265
|
+
serverLog("server", `Post-index work complete in ${postDuration}ms`);
|
|
16091
16266
|
}
|
|
16092
16267
|
if (process.argv.includes("--init-semantic")) {
|
|
16093
16268
|
(async () => {
|
package/package.json
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
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
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"flywheel-memory": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/velvetmonkey/flywheel-memory.git",
|
|
13
|
-
"directory": "packages/mcp-server"
|
|
14
|
-
},
|
|
15
|
-
"bugs": {
|
|
16
|
-
"url": "https://github.com/velvetmonkey/flywheel-memory/issues"
|
|
17
|
-
},
|
|
18
|
-
"homepage": "https://github.com/velvetmonkey/flywheel-memory#readme",
|
|
19
|
-
"author": "velvetmonkey",
|
|
20
|
-
"keywords": [
|
|
21
|
-
"mcp",
|
|
22
|
-
"mcp-server",
|
|
23
|
-
"obsidian",
|
|
24
|
-
"pkm",
|
|
25
|
-
"markdown",
|
|
26
|
-
"knowledge-graph",
|
|
27
|
-
"wikilinks",
|
|
28
|
-
"backlinks",
|
|
29
|
-
"vault",
|
|
30
|
-
"claude",
|
|
31
|
-
"claude-code",
|
|
32
|
-
"local-first",
|
|
33
|
-
"daily-notes",
|
|
34
|
-
"zettelkasten"
|
|
35
|
-
],
|
|
36
|
-
"scripts": {
|
|
37
|
-
"build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external && chmod +x dist/index.js",
|
|
38
|
-
"dev": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external --watch",
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"test:watch": "vitest",
|
|
41
|
-
"test:read": "vitest run test/read/",
|
|
42
|
-
"test:write": "vitest run test/write/",
|
|
43
|
-
"test:security": "vitest run test/write/security/",
|
|
44
|
-
"test:stress": "vitest run test/write/stress/ test/write/battle-hardening/",
|
|
45
|
-
"test:coverage": "vitest run --coverage",
|
|
46
|
-
"test:ci": "vitest run --reporter=github-actions",
|
|
47
|
-
"lint": "tsc --noEmit",
|
|
48
|
-
"clean": "rm -rf dist",
|
|
49
|
-
"prepublishOnly": "npm run build"
|
|
50
|
-
},
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
53
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
54
|
-
"better-sqlite3": "^11.0.0",
|
|
55
|
-
"chokidar": "^4.0.0",
|
|
56
|
-
"gray-matter": "^4.0.3",
|
|
57
|
-
"simple-git": "^3.22.0",
|
|
58
|
-
"zod": "^3.22.4",
|
|
59
|
-
"@huggingface/transformers": "^3.8.1"
|
|
60
|
-
},
|
|
61
|
-
"devDependencies": {
|
|
62
|
-
"@types/better-sqlite3": "^7.6.0",
|
|
63
|
-
"@types/node": "^20.10.0",
|
|
64
|
-
"@vitest/coverage-v8": "^2.0.0",
|
|
65
|
-
"esbuild": "^0.24.0",
|
|
66
|
-
"fast-check": "^3.15.0",
|
|
67
|
-
"mcp-testing-kit": "^0.2.0",
|
|
68
|
-
"tsx": "^4.19.0",
|
|
69
|
-
"typescript": "^5.3.2",
|
|
70
|
-
"vitest": "^2.0.0"
|
|
71
|
-
},
|
|
72
|
-
"engines": {
|
|
73
|
-
"node": ">=18.0.0"
|
|
74
|
-
},
|
|
75
|
-
"license": "AGPL-3.0-only",
|
|
76
|
-
"files": [
|
|
77
|
-
"dist",
|
|
78
|
-
"README.md",
|
|
79
|
-
"LICENSE"
|
|
80
|
-
],
|
|
81
|
-
"publishConfig": {
|
|
82
|
-
"access": "public"
|
|
83
|
-
}
|
|
84
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
+
"version": "2.0.31",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"flywheel-memory": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/velvetmonkey/flywheel-memory.git",
|
|
13
|
+
"directory": "packages/mcp-server"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/velvetmonkey/flywheel-memory/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/velvetmonkey/flywheel-memory#readme",
|
|
19
|
+
"author": "velvetmonkey",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"mcp-server",
|
|
23
|
+
"obsidian",
|
|
24
|
+
"pkm",
|
|
25
|
+
"markdown",
|
|
26
|
+
"knowledge-graph",
|
|
27
|
+
"wikilinks",
|
|
28
|
+
"backlinks",
|
|
29
|
+
"vault",
|
|
30
|
+
"claude",
|
|
31
|
+
"claude-code",
|
|
32
|
+
"local-first",
|
|
33
|
+
"daily-notes",
|
|
34
|
+
"zettelkasten"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external && chmod +x dist/index.js",
|
|
38
|
+
"dev": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external --watch",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"test:read": "vitest run test/read/",
|
|
42
|
+
"test:write": "vitest run test/write/",
|
|
43
|
+
"test:security": "vitest run test/write/security/",
|
|
44
|
+
"test:stress": "vitest run test/write/stress/ test/write/battle-hardening/",
|
|
45
|
+
"test:coverage": "vitest run --coverage",
|
|
46
|
+
"test:ci": "vitest run --reporter=github-actions",
|
|
47
|
+
"lint": "tsc --noEmit",
|
|
48
|
+
"clean": "rm -rf dist",
|
|
49
|
+
"prepublishOnly": "npm run build"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
53
|
+
"@velvetmonkey/vault-core": "^2.0.31",
|
|
54
|
+
"better-sqlite3": "^11.0.0",
|
|
55
|
+
"chokidar": "^4.0.0",
|
|
56
|
+
"gray-matter": "^4.0.3",
|
|
57
|
+
"simple-git": "^3.22.0",
|
|
58
|
+
"zod": "^3.22.4",
|
|
59
|
+
"@huggingface/transformers": "^3.8.1"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
63
|
+
"@types/node": "^20.10.0",
|
|
64
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
65
|
+
"esbuild": "^0.24.0",
|
|
66
|
+
"fast-check": "^3.15.0",
|
|
67
|
+
"mcp-testing-kit": "^0.2.0",
|
|
68
|
+
"tsx": "^4.19.0",
|
|
69
|
+
"typescript": "^5.3.2",
|
|
70
|
+
"vitest": "^2.0.0"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=18.0.0"
|
|
74
|
+
},
|
|
75
|
+
"license": "AGPL-3.0-only",
|
|
76
|
+
"files": [
|
|
77
|
+
"dist",
|
|
78
|
+
"README.md",
|
|
79
|
+
"LICENSE"
|
|
80
|
+
],
|
|
81
|
+
"publishConfig": {
|
|
82
|
+
"access": "public"
|
|
83
|
+
}
|
|
84
|
+
}
|