@velvetmonkey/flywheel-memory 2.0.28 → 2.0.30
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 +396 -135
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -476,7 +476,10 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
476
476
|
throw new Error("Invalid path: path traversal not allowed");
|
|
477
477
|
}
|
|
478
478
|
const fullPath = path18.join(vaultPath2, notePath);
|
|
479
|
-
const rawContent = await
|
|
479
|
+
const [rawContent, stat3] = await Promise.all([
|
|
480
|
+
fs18.readFile(fullPath, "utf-8"),
|
|
481
|
+
fs18.stat(fullPath)
|
|
482
|
+
]);
|
|
480
483
|
const lineEnding = detectLineEnding(rawContent);
|
|
481
484
|
const normalizedContent = normalizeLineEndings(rawContent);
|
|
482
485
|
const parsed = matter5(normalizedContent);
|
|
@@ -485,7 +488,8 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
485
488
|
content: parsed.content,
|
|
486
489
|
frontmatter,
|
|
487
490
|
rawContent,
|
|
488
|
-
lineEnding
|
|
491
|
+
lineEnding,
|
|
492
|
+
mtimeMs: stat3.mtimeMs
|
|
489
493
|
};
|
|
490
494
|
}
|
|
491
495
|
function deepCloneFrontmatter(obj) {
|
|
@@ -2481,7 +2485,8 @@ import {
|
|
|
2481
2485
|
} from "@velvetmonkey/vault-core";
|
|
2482
2486
|
var DEFAULT_CONFIG = {
|
|
2483
2487
|
exclude_task_tags: [],
|
|
2484
|
-
exclude_analysis_tags: []
|
|
2488
|
+
exclude_analysis_tags: [],
|
|
2489
|
+
exclude_entities: []
|
|
2485
2490
|
};
|
|
2486
2491
|
function loadConfig(stateDb2) {
|
|
2487
2492
|
if (stateDb2) {
|
|
@@ -5894,36 +5899,38 @@ async function buildFTS5Index(vaultPath2) {
|
|
|
5894
5899
|
if (!db2) {
|
|
5895
5900
|
throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
|
|
5896
5901
|
}
|
|
5897
|
-
db2.exec("DELETE FROM notes_fts");
|
|
5898
5902
|
const files = await scanVault(vaultPath2);
|
|
5899
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
|
+
}
|
|
5900
5919
|
const insert = db2.prepare(
|
|
5901
5920
|
"INSERT INTO notes_fts (path, title, frontmatter, content) VALUES (?, ?, ?, ?)"
|
|
5902
5921
|
);
|
|
5903
|
-
const insertMany = db2.transaction((filesToIndex) => {
|
|
5904
|
-
let indexed2 = 0;
|
|
5905
|
-
for (const file of filesToIndex) {
|
|
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
|
-
insert.run(file.path, title, frontmatter, body);
|
|
5915
|
-
indexed2++;
|
|
5916
|
-
} catch (err) {
|
|
5917
|
-
console.error(`[FTS5] Skipping ${file.path}:`, err);
|
|
5918
|
-
}
|
|
5919
|
-
}
|
|
5920
|
-
return indexed2;
|
|
5921
|
-
});
|
|
5922
|
-
const indexed = insertMany(indexableFiles);
|
|
5923
5922
|
const now = /* @__PURE__ */ new Date();
|
|
5924
|
-
db2.
|
|
5925
|
-
"
|
|
5926
|
-
|
|
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;
|
|
5927
5934
|
state = {
|
|
5928
5935
|
ready: true,
|
|
5929
5936
|
building: false,
|
|
@@ -6183,6 +6190,9 @@ function setTaskCacheDatabase(database) {
|
|
|
6183
6190
|
function isTaskCacheReady() {
|
|
6184
6191
|
return cacheReady && db3 !== null;
|
|
6185
6192
|
}
|
|
6193
|
+
function isTaskCacheBuilding() {
|
|
6194
|
+
return rebuildInProgress;
|
|
6195
|
+
}
|
|
6186
6196
|
async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
6187
6197
|
if (!db3) {
|
|
6188
6198
|
throw new Error("Task cache database not initialized. Call setTaskCacheDatabase() first.");
|
|
@@ -6191,53 +6201,47 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
6191
6201
|
rebuildInProgress = true;
|
|
6192
6202
|
const start = Date.now();
|
|
6193
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
|
+
}
|
|
6194
6228
|
const insertStmt = db3.prepare(`
|
|
6195
6229
|
INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
|
|
6196
6230
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6197
6231
|
`);
|
|
6198
|
-
const
|
|
6232
|
+
const swapAll = db3.transaction(() => {
|
|
6199
6233
|
db3.prepare("DELETE FROM tasks").run();
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
const notePaths2 = [];
|
|
6203
|
-
for (const note of index.notes.values()) {
|
|
6204
|
-
notePaths2.push(note.path);
|
|
6234
|
+
for (const row of allRows) {
|
|
6235
|
+
insertStmt.run(...row);
|
|
6205
6236
|
}
|
|
6206
|
-
|
|
6237
|
+
db3.prepare(
|
|
6238
|
+
"INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
|
|
6239
|
+
).run("task_cache_built", (/* @__PURE__ */ new Date()).toISOString());
|
|
6207
6240
|
});
|
|
6208
|
-
|
|
6209
|
-
let totalTasks = 0;
|
|
6210
|
-
for (const notePath of notePaths) {
|
|
6211
|
-
const absolutePath = path10.join(vaultPath2, notePath);
|
|
6212
|
-
const tasks = await extractTasksFromNote(notePath, absolutePath);
|
|
6213
|
-
if (tasks.length > 0) {
|
|
6214
|
-
const insertBatch = db3.transaction(() => {
|
|
6215
|
-
for (const task of tasks) {
|
|
6216
|
-
if (excludeTags?.length && excludeTags.some((t) => task.tags.includes(t))) {
|
|
6217
|
-
continue;
|
|
6218
|
-
}
|
|
6219
|
-
stmt.run(
|
|
6220
|
-
task.path,
|
|
6221
|
-
task.line,
|
|
6222
|
-
task.text,
|
|
6223
|
-
task.status,
|
|
6224
|
-
task.raw,
|
|
6225
|
-
task.context ?? null,
|
|
6226
|
-
task.tags.length > 0 ? JSON.stringify(task.tags) : null,
|
|
6227
|
-
task.due_date ?? null
|
|
6228
|
-
);
|
|
6229
|
-
totalTasks++;
|
|
6230
|
-
}
|
|
6231
|
-
});
|
|
6232
|
-
insertBatch();
|
|
6233
|
-
}
|
|
6234
|
-
}
|
|
6235
|
-
db3.prepare(
|
|
6236
|
-
"INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
|
|
6237
|
-
).run("task_cache_built", (/* @__PURE__ */ new Date()).toISOString());
|
|
6241
|
+
swapAll();
|
|
6238
6242
|
cacheReady = true;
|
|
6239
6243
|
const duration = Date.now() - start;
|
|
6240
|
-
serverLog("tasks", `Task cache built: ${
|
|
6244
|
+
serverLog("tasks", `Task cache built: ${allRows.length} tasks from ${notePaths.length} notes in ${duration}ms`);
|
|
6241
6245
|
} finally {
|
|
6242
6246
|
rebuildInProgress = false;
|
|
6243
6247
|
}
|
|
@@ -6671,10 +6675,20 @@ async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
|
|
|
6671
6675
|
try {
|
|
6672
6676
|
const fullPath = path11.join(vaultPath2, sourcePath);
|
|
6673
6677
|
const content = await fs9.promises.readFile(fullPath, "utf-8");
|
|
6674
|
-
const
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
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();
|
|
6678
6692
|
} catch {
|
|
6679
6693
|
return "";
|
|
6680
6694
|
}
|
|
@@ -7299,10 +7313,28 @@ function getActivitySummary(index, days) {
|
|
|
7299
7313
|
import { SCHEMA_VERSION } from "@velvetmonkey/vault-core";
|
|
7300
7314
|
|
|
7301
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
|
+
}
|
|
7302
7334
|
function recordIndexEvent(stateDb2, event) {
|
|
7303
7335
|
stateDb2.db.prepare(
|
|
7304
|
-
`INSERT INTO index_events (timestamp, trigger, duration_ms, success, note_count, files_changed, changed_paths, error)
|
|
7305
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7336
|
+
`INSERT INTO index_events (timestamp, trigger, duration_ms, success, note_count, files_changed, changed_paths, error, steps)
|
|
7337
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7306
7338
|
).run(
|
|
7307
7339
|
Date.now(),
|
|
7308
7340
|
event.trigger,
|
|
@@ -7311,7 +7343,8 @@ function recordIndexEvent(stateDb2, event) {
|
|
|
7311
7343
|
event.note_count ?? null,
|
|
7312
7344
|
event.files_changed ?? null,
|
|
7313
7345
|
event.changed_paths ? JSON.stringify(event.changed_paths) : null,
|
|
7314
|
-
event.error ?? null
|
|
7346
|
+
event.error ?? null,
|
|
7347
|
+
event.steps ? JSON.stringify(event.steps) : null
|
|
7315
7348
|
);
|
|
7316
7349
|
}
|
|
7317
7350
|
function rowToEvent(row) {
|
|
@@ -7324,7 +7357,8 @@ function rowToEvent(row) {
|
|
|
7324
7357
|
note_count: row.note_count,
|
|
7325
7358
|
files_changed: row.files_changed,
|
|
7326
7359
|
changed_paths: row.changed_paths ? JSON.parse(row.changed_paths) : null,
|
|
7327
|
-
error: row.error
|
|
7360
|
+
error: row.error,
|
|
7361
|
+
steps: row.steps ? JSON.parse(row.steps) : null
|
|
7328
7362
|
};
|
|
7329
7363
|
}
|
|
7330
7364
|
function getRecentIndexEvents(stateDb2, limit = 20) {
|
|
@@ -7402,6 +7436,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7402
7436
|
note_count: z3.coerce.number().describe("Number of notes in the index"),
|
|
7403
7437
|
entity_count: z3.coerce.number().describe("Number of linkable entities (titles + aliases)"),
|
|
7404
7438
|
tag_count: z3.coerce.number().describe("Number of unique tags"),
|
|
7439
|
+
link_count: z3.coerce.number().describe("Total number of outgoing wikilinks"),
|
|
7405
7440
|
periodic_notes: z3.array(PeriodicNoteInfoSchema).optional().describe("Detected periodic note conventions"),
|
|
7406
7441
|
config: z3.record(z3.unknown()).optional().describe("Current flywheel config (paths, templates, etc.)"),
|
|
7407
7442
|
last_rebuild: z3.object({
|
|
@@ -7410,11 +7445,27 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7410
7445
|
duration_ms: z3.number(),
|
|
7411
7446
|
ago_seconds: z3.number()
|
|
7412
7447
|
}).optional().describe("Most recent index rebuild event"),
|
|
7448
|
+
last_pipeline: z3.object({
|
|
7449
|
+
timestamp: z3.number(),
|
|
7450
|
+
trigger: z3.string(),
|
|
7451
|
+
duration_ms: z3.number(),
|
|
7452
|
+
files_changed: z3.number().nullable(),
|
|
7453
|
+
steps: z3.array(z3.object({
|
|
7454
|
+
name: z3.string(),
|
|
7455
|
+
duration_ms: z3.number(),
|
|
7456
|
+
input: z3.record(z3.unknown()),
|
|
7457
|
+
output: z3.record(z3.unknown()),
|
|
7458
|
+
skipped: z3.boolean().optional(),
|
|
7459
|
+
skip_reason: z3.string().optional()
|
|
7460
|
+
}))
|
|
7461
|
+
}).optional().describe("Most recent watcher pipeline run with per-step timing"),
|
|
7413
7462
|
fts5_ready: z3.boolean().describe("Whether the FTS5 keyword search index is ready"),
|
|
7414
7463
|
fts5_building: z3.boolean().describe("Whether the FTS5 keyword search index is currently building"),
|
|
7415
7464
|
embeddings_building: z3.boolean().describe("Whether semantic embeddings are currently building"),
|
|
7416
7465
|
embeddings_ready: z3.boolean().describe("Whether semantic embeddings have been built (enables hybrid keyword+semantic search)"),
|
|
7417
7466
|
embeddings_count: z3.coerce.number().describe("Number of notes with semantic embeddings"),
|
|
7467
|
+
tasks_ready: z3.boolean().describe("Whether the task cache is ready to serve queries"),
|
|
7468
|
+
tasks_building: z3.boolean().describe("Whether the task cache is currently rebuilding"),
|
|
7418
7469
|
recommendations: z3.array(z3.string()).describe("Suggested actions if any issues detected")
|
|
7419
7470
|
};
|
|
7420
7471
|
server2.registerTool(
|
|
@@ -7455,6 +7506,10 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7455
7506
|
const noteCount = indexBuilt ? index.notes.size : 0;
|
|
7456
7507
|
const entityCount = indexBuilt ? index.entities.size : 0;
|
|
7457
7508
|
const tagCount = indexBuilt ? index.tags.size : 0;
|
|
7509
|
+
let linkCount = 0;
|
|
7510
|
+
if (indexBuilt) {
|
|
7511
|
+
for (const note of index.notes.values()) linkCount += note.outlinks.length;
|
|
7512
|
+
}
|
|
7458
7513
|
if (indexBuilt && noteCount === 0 && vaultAccessible) {
|
|
7459
7514
|
recommendations.push("No notes found in vault. Is PROJECT_PATH pointing to a markdown vault?");
|
|
7460
7515
|
}
|
|
@@ -7500,6 +7555,23 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7500
7555
|
} catch {
|
|
7501
7556
|
}
|
|
7502
7557
|
}
|
|
7558
|
+
let lastPipeline;
|
|
7559
|
+
if (stateDb2) {
|
|
7560
|
+
try {
|
|
7561
|
+
const events = getRecentIndexEvents(stateDb2, 1);
|
|
7562
|
+
if (events.length > 0 && events[0].steps && events[0].steps.length > 0) {
|
|
7563
|
+
const evt = events[0];
|
|
7564
|
+
lastPipeline = {
|
|
7565
|
+
timestamp: evt.timestamp,
|
|
7566
|
+
trigger: evt.trigger,
|
|
7567
|
+
duration_ms: evt.duration_ms,
|
|
7568
|
+
files_changed: evt.files_changed,
|
|
7569
|
+
steps: evt.steps
|
|
7570
|
+
};
|
|
7571
|
+
}
|
|
7572
|
+
} catch {
|
|
7573
|
+
}
|
|
7574
|
+
}
|
|
7503
7575
|
const ftsState = getFTS5State();
|
|
7504
7576
|
const output = {
|
|
7505
7577
|
status,
|
|
@@ -7515,14 +7587,18 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7515
7587
|
note_count: noteCount,
|
|
7516
7588
|
entity_count: entityCount,
|
|
7517
7589
|
tag_count: tagCount,
|
|
7590
|
+
link_count: linkCount,
|
|
7518
7591
|
periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
|
|
7519
7592
|
config: configInfo,
|
|
7520
7593
|
last_rebuild: lastRebuild,
|
|
7594
|
+
last_pipeline: lastPipeline,
|
|
7521
7595
|
fts5_ready: ftsState.ready,
|
|
7522
7596
|
fts5_building: ftsState.building,
|
|
7523
7597
|
embeddings_building: isEmbeddingsBuilding(),
|
|
7524
7598
|
embeddings_ready: hasEmbeddingsIndex(),
|
|
7525
7599
|
embeddings_count: getEmbeddingsCount(),
|
|
7600
|
+
tasks_ready: isTaskCacheReady(),
|
|
7601
|
+
tasks_building: isTaskCacheBuilding(),
|
|
7526
7602
|
recommendations
|
|
7527
7603
|
};
|
|
7528
7604
|
return {
|
|
@@ -7999,6 +8075,64 @@ import * as fs11 from "fs";
|
|
|
7999
8075
|
import * as path12 from "path";
|
|
8000
8076
|
import { z as z5 } from "zod";
|
|
8001
8077
|
import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
|
|
8078
|
+
|
|
8079
|
+
// src/core/read/aliasSuggestions.ts
|
|
8080
|
+
function generateAliasCandidates(entityName, existingAliases) {
|
|
8081
|
+
const existing = new Set(existingAliases.map((a) => a.toLowerCase()));
|
|
8082
|
+
const candidates = [];
|
|
8083
|
+
const words = entityName.split(/[\s-]+/).filter((w) => w.length > 0);
|
|
8084
|
+
if (words.length >= 2) {
|
|
8085
|
+
const acronym = words.map((w) => w[0]).join("").toUpperCase();
|
|
8086
|
+
if (acronym.length >= 2 && acronym.length <= 6 && !existing.has(acronym.toLowerCase())) {
|
|
8087
|
+
candidates.push({ candidate: acronym, type: "acronym" });
|
|
8088
|
+
}
|
|
8089
|
+
if (words.length >= 3) {
|
|
8090
|
+
const short = words[0];
|
|
8091
|
+
if (short.length >= 3 && !existing.has(short.toLowerCase())) {
|
|
8092
|
+
candidates.push({ candidate: short, type: "short_form" });
|
|
8093
|
+
}
|
|
8094
|
+
}
|
|
8095
|
+
}
|
|
8096
|
+
return candidates;
|
|
8097
|
+
}
|
|
8098
|
+
function suggestEntityAliases(stateDb2, folder) {
|
|
8099
|
+
const db4 = stateDb2.db;
|
|
8100
|
+
const entities = folder ? db4.prepare(
|
|
8101
|
+
"SELECT name, path, aliases_json FROM entities WHERE path LIKE ? || '/%'"
|
|
8102
|
+
).all(folder) : db4.prepare("SELECT name, path, aliases_json FROM entities").all();
|
|
8103
|
+
const allEntityNames = new Set(
|
|
8104
|
+
db4.prepare("SELECT name_lower FROM entities").all().map((r) => r.name_lower)
|
|
8105
|
+
);
|
|
8106
|
+
const suggestions = [];
|
|
8107
|
+
const countStmt = db4.prepare(
|
|
8108
|
+
"SELECT COUNT(*) as cnt FROM notes_fts WHERE content MATCH ?"
|
|
8109
|
+
);
|
|
8110
|
+
for (const row of entities) {
|
|
8111
|
+
const aliases = row.aliases_json ? JSON.parse(row.aliases_json) : [];
|
|
8112
|
+
const candidates = generateAliasCandidates(row.name, aliases);
|
|
8113
|
+
for (const { candidate, type } of candidates) {
|
|
8114
|
+
if (allEntityNames.has(candidate.toLowerCase())) continue;
|
|
8115
|
+
let mentions = 0;
|
|
8116
|
+
try {
|
|
8117
|
+
const result = countStmt.get(`"${candidate}"`);
|
|
8118
|
+
mentions = result?.cnt ?? 0;
|
|
8119
|
+
} catch {
|
|
8120
|
+
}
|
|
8121
|
+
suggestions.push({
|
|
8122
|
+
entity: row.name,
|
|
8123
|
+
entity_path: row.path,
|
|
8124
|
+
current_aliases: aliases,
|
|
8125
|
+
candidate,
|
|
8126
|
+
type,
|
|
8127
|
+
mentions
|
|
8128
|
+
});
|
|
8129
|
+
}
|
|
8130
|
+
}
|
|
8131
|
+
suggestions.sort((a, b) => b.mentions - a.mentions || a.entity.localeCompare(b.entity));
|
|
8132
|
+
return suggestions;
|
|
8133
|
+
}
|
|
8134
|
+
|
|
8135
|
+
// src/tools/read/system.ts
|
|
8002
8136
|
function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
|
|
8003
8137
|
const RefreshIndexOutputSchema = {
|
|
8004
8138
|
success: z5.boolean().describe("Whether the refresh succeeded"),
|
|
@@ -8485,6 +8619,35 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
8485
8619
|
};
|
|
8486
8620
|
}
|
|
8487
8621
|
);
|
|
8622
|
+
server2.registerTool(
|
|
8623
|
+
"suggest_entity_aliases",
|
|
8624
|
+
{
|
|
8625
|
+
title: "Suggest Entity Aliases",
|
|
8626
|
+
description: "Generate alias suggestions for entities in a folder based on acronyms and short forms, validated against vault content.",
|
|
8627
|
+
inputSchema: {
|
|
8628
|
+
folder: z5.string().optional().describe("Folder path to scope suggestions to"),
|
|
8629
|
+
limit: z5.number().default(20).describe("Max suggestions to return")
|
|
8630
|
+
}
|
|
8631
|
+
},
|
|
8632
|
+
async ({
|
|
8633
|
+
folder,
|
|
8634
|
+
limit: requestedLimit
|
|
8635
|
+
}) => {
|
|
8636
|
+
const stateDb2 = getStateDb?.();
|
|
8637
|
+
if (!stateDb2) {
|
|
8638
|
+
return {
|
|
8639
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
8640
|
+
};
|
|
8641
|
+
}
|
|
8642
|
+
const suggestions = suggestEntityAliases(stateDb2, folder || void 0);
|
|
8643
|
+
const limit = Math.min(requestedLimit ?? 20, 50);
|
|
8644
|
+
const limited = suggestions.slice(0, limit);
|
|
8645
|
+
const output = { suggestion_count: limited.length, suggestions: limited };
|
|
8646
|
+
return {
|
|
8647
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
8648
|
+
};
|
|
8649
|
+
}
|
|
8650
|
+
);
|
|
8488
8651
|
}
|
|
8489
8652
|
|
|
8490
8653
|
// src/tools/read/primitives.ts
|
|
@@ -8799,6 +8962,8 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
8799
8962
|
content: [{ type: "text", text: JSON.stringify({
|
|
8800
8963
|
total_count: result2.total,
|
|
8801
8964
|
open_count: result2.open_count,
|
|
8965
|
+
completed_count: result2.completed_count,
|
|
8966
|
+
cancelled_count: result2.cancelled_count,
|
|
8802
8967
|
returned_count: result2.tasks.length,
|
|
8803
8968
|
tasks: result2.tasks
|
|
8804
8969
|
}, null, 2) }]
|
|
@@ -8831,6 +8996,8 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
8831
8996
|
content: [{ type: "text", text: JSON.stringify({
|
|
8832
8997
|
total_count: result.total,
|
|
8833
8998
|
open_count: result.open_count,
|
|
8999
|
+
completed_count: result.completed_count,
|
|
9000
|
+
cancelled_count: result.cancelled_count,
|
|
8834
9001
|
returned_count: paged.length,
|
|
8835
9002
|
tasks: paged
|
|
8836
9003
|
}, null, 2) }]
|
|
@@ -9686,6 +9853,35 @@ function isPeriodicNote(notePath) {
|
|
|
9686
9853
|
const folder = notePath.split("/")[0]?.toLowerCase() || "";
|
|
9687
9854
|
return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
|
|
9688
9855
|
}
|
|
9856
|
+
function getExcludedPaths(index, config) {
|
|
9857
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
9858
|
+
const excludeTags = new Set((config.exclude_analysis_tags ?? []).map((t) => t.toLowerCase()));
|
|
9859
|
+
const excludeEntities = new Set((config.exclude_entities ?? []).map((e) => e.toLowerCase()));
|
|
9860
|
+
if (excludeTags.size === 0 && excludeEntities.size === 0) return excluded;
|
|
9861
|
+
for (const note of index.notes.values()) {
|
|
9862
|
+
if (excludeTags.size > 0) {
|
|
9863
|
+
const tags = note.frontmatter?.tags;
|
|
9864
|
+
const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
|
|
9865
|
+
if (tagList.some((t) => excludeTags.has(String(t).toLowerCase()))) {
|
|
9866
|
+
excluded.add(note.path);
|
|
9867
|
+
continue;
|
|
9868
|
+
}
|
|
9869
|
+
}
|
|
9870
|
+
if (excludeEntities.size > 0) {
|
|
9871
|
+
if (excludeEntities.has(note.title.toLowerCase())) {
|
|
9872
|
+
excluded.add(note.path);
|
|
9873
|
+
continue;
|
|
9874
|
+
}
|
|
9875
|
+
for (const alias of note.aliases) {
|
|
9876
|
+
if (excludeEntities.has(alias.toLowerCase())) {
|
|
9877
|
+
excluded.add(note.path);
|
|
9878
|
+
break;
|
|
9879
|
+
}
|
|
9880
|
+
}
|
|
9881
|
+
}
|
|
9882
|
+
}
|
|
9883
|
+
return excluded;
|
|
9884
|
+
}
|
|
9689
9885
|
function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb, getConfig) {
|
|
9690
9886
|
server2.registerTool(
|
|
9691
9887
|
"graph_analysis",
|
|
@@ -9707,9 +9903,11 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9707
9903
|
requireIndex();
|
|
9708
9904
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
9709
9905
|
const index = getIndex();
|
|
9906
|
+
const config = getConfig?.() ?? {};
|
|
9907
|
+
const excludedPaths = getExcludedPaths(index, config);
|
|
9710
9908
|
switch (analysis) {
|
|
9711
9909
|
case "orphans": {
|
|
9712
|
-
const allOrphans = findOrphanNotes(index, folder).filter((o) => !isPeriodicNote(o.path));
|
|
9910
|
+
const allOrphans = findOrphanNotes(index, folder).filter((o) => !isPeriodicNote(o.path) && !excludedPaths.has(o.path));
|
|
9713
9911
|
const orphans = allOrphans.slice(offset, offset + limit);
|
|
9714
9912
|
return {
|
|
9715
9913
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -9726,7 +9924,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9726
9924
|
};
|
|
9727
9925
|
}
|
|
9728
9926
|
case "dead_ends": {
|
|
9729
|
-
const allResults = findDeadEnds(index, folder, min_backlinks);
|
|
9927
|
+
const allResults = findDeadEnds(index, folder, min_backlinks).filter((n) => !excludedPaths.has(n.path));
|
|
9730
9928
|
const result = allResults.slice(offset, offset + limit);
|
|
9731
9929
|
return {
|
|
9732
9930
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -9739,7 +9937,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9739
9937
|
};
|
|
9740
9938
|
}
|
|
9741
9939
|
case "sources": {
|
|
9742
|
-
const allResults = findSources(index, folder, min_outlinks);
|
|
9940
|
+
const allResults = findSources(index, folder, min_outlinks).filter((n) => !excludedPaths.has(n.path));
|
|
9743
9941
|
const result = allResults.slice(offset, offset + limit);
|
|
9744
9942
|
return {
|
|
9745
9943
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -9752,17 +9950,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9752
9950
|
};
|
|
9753
9951
|
}
|
|
9754
9952
|
case "hubs": {
|
|
9755
|
-
const
|
|
9756
|
-
(getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
|
|
9757
|
-
);
|
|
9758
|
-
const allHubs = findHubNotes(index, min_links).filter((h) => {
|
|
9759
|
-
if (excludeTags.size === 0) return true;
|
|
9760
|
-
const note = index.notes.get(h.path);
|
|
9761
|
-
if (!note) return true;
|
|
9762
|
-
const tags = note.frontmatter?.tags;
|
|
9763
|
-
const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
|
|
9764
|
-
return !tagList.some((t) => excludeTags.has(String(t).toLowerCase()));
|
|
9765
|
-
});
|
|
9953
|
+
const allHubs = findHubNotes(index, min_links).filter((h) => !excludedPaths.has(h.path));
|
|
9766
9954
|
const hubs = allHubs.slice(offset, offset + limit);
|
|
9767
9955
|
return {
|
|
9768
9956
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -9788,7 +9976,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9788
9976
|
}, null, 2) }]
|
|
9789
9977
|
};
|
|
9790
9978
|
}
|
|
9791
|
-
const result = getStaleNotes(index, days, min_backlinks).slice(0, limit);
|
|
9979
|
+
const result = getStaleNotes(index, days, min_backlinks).filter((n) => !excludedPaths.has(n.path)).slice(0, limit);
|
|
9792
9980
|
return {
|
|
9793
9981
|
content: [{ type: "text", text: JSON.stringify({
|
|
9794
9982
|
analysis: "stale",
|
|
@@ -9804,7 +9992,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9804
9992
|
case "immature": {
|
|
9805
9993
|
const vaultPath2 = getVaultPath();
|
|
9806
9994
|
const allNotes = Array.from(index.notes.values()).filter(
|
|
9807
|
-
(note) => (!folder || note.path.startsWith(folder + "/") || note.path.substring(0, note.path.lastIndexOf("/")) === folder) && !isPeriodicNote(note.path)
|
|
9995
|
+
(note) => (!folder || note.path.startsWith(folder + "/") || note.path.substring(0, note.path.lastIndexOf("/")) === folder) && !isPeriodicNote(note.path) && !excludedPaths.has(note.path)
|
|
9808
9996
|
);
|
|
9809
9997
|
const conventions = inferFolderConventions(index, folder, 0.5);
|
|
9810
9998
|
const expectedFields = conventions.inferred_fields.map((f) => f.name);
|
|
@@ -9889,22 +10077,20 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
9889
10077
|
}
|
|
9890
10078
|
const daysBack = days ?? 30;
|
|
9891
10079
|
let hubs = getEmergingHubs(db4, daysBack);
|
|
9892
|
-
|
|
9893
|
-
(getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
|
|
9894
|
-
);
|
|
9895
|
-
if (excludeTags.size > 0) {
|
|
10080
|
+
if (excludedPaths.size > 0) {
|
|
9896
10081
|
const notesByTitle = /* @__PURE__ */ new Map();
|
|
9897
10082
|
for (const note of index.notes.values()) {
|
|
9898
10083
|
notesByTitle.set(note.title.toLowerCase(), note);
|
|
9899
10084
|
}
|
|
9900
10085
|
hubs = hubs.filter((hub) => {
|
|
9901
10086
|
const note = notesByTitle.get(hub.entity.toLowerCase());
|
|
9902
|
-
|
|
9903
|
-
const tags = note.frontmatter?.tags;
|
|
9904
|
-
const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
|
|
9905
|
-
return !tagList.some((t) => excludeTags.has(String(t).toLowerCase()));
|
|
10087
|
+
return !note || !excludedPaths.has(note.path);
|
|
9906
10088
|
});
|
|
9907
10089
|
}
|
|
10090
|
+
const excludeEntities = new Set((config.exclude_entities ?? []).map((e) => e.toLowerCase()));
|
|
10091
|
+
if (excludeEntities.size > 0) {
|
|
10092
|
+
hubs = hubs.filter((hub) => !excludeEntities.has(hub.entity.toLowerCase()));
|
|
10093
|
+
}
|
|
9908
10094
|
return {
|
|
9909
10095
|
content: [{ type: "text", text: JSON.stringify({
|
|
9910
10096
|
analysis: "emerging_hubs",
|
|
@@ -11141,41 +11327,58 @@ async function withVaultFile(options, operation) {
|
|
|
11141
11327
|
if (existsError) {
|
|
11142
11328
|
return formatMcpResult(existsError);
|
|
11143
11329
|
}
|
|
11144
|
-
const
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11330
|
+
const runMutation = async () => {
|
|
11331
|
+
const { content, frontmatter: frontmatter2, lineEnding: lineEnding2, mtimeMs } = await readVaultFile(vaultPath2, notePath);
|
|
11332
|
+
const writeStateDb = getWriteStateDb();
|
|
11333
|
+
if (writeStateDb) {
|
|
11334
|
+
processImplicitFeedback(writeStateDb, notePath, content);
|
|
11335
|
+
}
|
|
11336
|
+
let sectionBoundary;
|
|
11337
|
+
if (section) {
|
|
11338
|
+
const sectionResult = ensureSectionExists(content, section, notePath);
|
|
11339
|
+
if ("error" in sectionResult) {
|
|
11340
|
+
return { error: sectionResult.error };
|
|
11341
|
+
}
|
|
11342
|
+
sectionBoundary = sectionResult.boundary;
|
|
11343
|
+
}
|
|
11344
|
+
const ctx = {
|
|
11345
|
+
content,
|
|
11346
|
+
frontmatter: frontmatter2,
|
|
11347
|
+
lineEnding: lineEnding2,
|
|
11348
|
+
sectionBoundary,
|
|
11349
|
+
vaultPath: vaultPath2,
|
|
11350
|
+
notePath
|
|
11351
|
+
};
|
|
11352
|
+
const opResult2 = await operation(ctx);
|
|
11353
|
+
return { opResult: opResult2, frontmatter: frontmatter2, lineEnding: lineEnding2, mtimeMs };
|
|
11354
|
+
};
|
|
11355
|
+
let result = await runMutation();
|
|
11356
|
+
if ("error" in result) {
|
|
11357
|
+
return formatMcpResult(result.error);
|
|
11148
11358
|
}
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11359
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
11360
|
+
const statBefore = await fs19.stat(fullPath);
|
|
11361
|
+
if (statBefore.mtimeMs !== result.mtimeMs) {
|
|
11362
|
+
console.warn(`[withVaultFile] External modification detected on ${notePath}, re-reading and retrying`);
|
|
11363
|
+
result = await runMutation();
|
|
11364
|
+
if ("error" in result) {
|
|
11365
|
+
return formatMcpResult(result.error);
|
|
11154
11366
|
}
|
|
11155
|
-
sectionBoundary = sectionResult.boundary;
|
|
11156
11367
|
}
|
|
11157
|
-
const
|
|
11158
|
-
content,
|
|
11159
|
-
frontmatter,
|
|
11160
|
-
lineEnding,
|
|
11161
|
-
sectionBoundary,
|
|
11162
|
-
vaultPath: vaultPath2,
|
|
11163
|
-
notePath
|
|
11164
|
-
};
|
|
11165
|
-
const opResult = await operation(ctx);
|
|
11368
|
+
const { opResult, frontmatter, lineEnding } = result;
|
|
11166
11369
|
let finalFrontmatter = opResult.updatedFrontmatter ?? frontmatter;
|
|
11167
11370
|
if (scoping && (scoping.agent_id || scoping.session_id)) {
|
|
11168
11371
|
finalFrontmatter = injectMutationMetadata(finalFrontmatter, scoping);
|
|
11169
11372
|
}
|
|
11170
11373
|
await writeVaultFile(vaultPath2, notePath, opResult.updatedContent, finalFrontmatter, lineEnding);
|
|
11171
11374
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, commitPrefix);
|
|
11172
|
-
const
|
|
11375
|
+
const successRes = successResult(notePath, opResult.message, gitInfo, {
|
|
11173
11376
|
preview: opResult.preview,
|
|
11174
11377
|
warnings: opResult.warnings,
|
|
11175
11378
|
outputIssues: opResult.outputIssues,
|
|
11176
11379
|
normalizationChanges: opResult.normalizationChanges
|
|
11177
11380
|
});
|
|
11178
|
-
return formatMcpResult(
|
|
11381
|
+
return formatMcpResult(successRes);
|
|
11179
11382
|
} catch (error) {
|
|
11180
11383
|
const extras = {};
|
|
11181
11384
|
if (error instanceof DiagnosticError) {
|
|
@@ -15664,6 +15867,7 @@ async function main() {
|
|
|
15664
15867
|
setFTS5Database(stateDb.db);
|
|
15665
15868
|
setEmbeddingsDatabase(stateDb.db);
|
|
15666
15869
|
setTaskCacheDatabase(stateDb.db);
|
|
15870
|
+
serverLog("statedb", "Injected FTS5, embeddings, task cache handles");
|
|
15667
15871
|
loadEntityEmbeddingsToMemory();
|
|
15668
15872
|
setWriteStateDb(stateDb);
|
|
15669
15873
|
} catch (err) {
|
|
@@ -15712,7 +15916,8 @@ async function main() {
|
|
|
15712
15916
|
vaultIndex = cachedIndex;
|
|
15713
15917
|
setIndexState("ready");
|
|
15714
15918
|
const duration = Date.now() - startTime;
|
|
15715
|
-
|
|
15919
|
+
const cacheAge = cachedIndex.builtAt ? Math.round((Date.now() - cachedIndex.builtAt.getTime()) / 1e3) : 0;
|
|
15920
|
+
serverLog("index", `Cache hit: ${cachedIndex.notes.size} notes, ${cacheAge}s old \u2014 loaded in ${duration}ms`);
|
|
15716
15921
|
if (stateDb) {
|
|
15717
15922
|
recordIndexEvent(stateDb, {
|
|
15718
15923
|
trigger: "startup_cache",
|
|
@@ -15722,7 +15927,7 @@ async function main() {
|
|
|
15722
15927
|
}
|
|
15723
15928
|
runPostIndexWork(vaultIndex);
|
|
15724
15929
|
} else {
|
|
15725
|
-
serverLog("index", "
|
|
15930
|
+
serverLog("index", "Cache miss: building from scratch");
|
|
15726
15931
|
try {
|
|
15727
15932
|
vaultIndex = await buildVaultIndex(vaultPath);
|
|
15728
15933
|
setIndexState("ready");
|
|
@@ -15793,9 +15998,13 @@ async function updateEntitiesInStateDb() {
|
|
|
15793
15998
|
}
|
|
15794
15999
|
}
|
|
15795
16000
|
async function runPostIndexWork(index) {
|
|
16001
|
+
const postStart = Date.now();
|
|
16002
|
+
serverLog("index", "Scanning entities...");
|
|
15796
16003
|
await updateEntitiesInStateDb();
|
|
15797
16004
|
await initializeEntityIndex(vaultPath);
|
|
16005
|
+
serverLog("index", "Entity index initialized");
|
|
15798
16006
|
await exportHubScores(index, stateDb);
|
|
16007
|
+
serverLog("index", "Hub scores exported");
|
|
15799
16008
|
if (stateDb) {
|
|
15800
16009
|
try {
|
|
15801
16010
|
const metrics = computeMetrics(index, stateDb);
|
|
@@ -15820,6 +16029,7 @@ async function runPostIndexWork(index) {
|
|
|
15820
16029
|
if (stateDb) {
|
|
15821
16030
|
try {
|
|
15822
16031
|
updateSuppressionList(stateDb);
|
|
16032
|
+
serverLog("index", "Suppression list updated");
|
|
15823
16033
|
} catch (err) {
|
|
15824
16034
|
serverLog("server", `Failed to update suppression list: ${err instanceof Error ? err.message : err}`, "error");
|
|
15825
16035
|
}
|
|
@@ -15830,9 +16040,15 @@ async function runPostIndexWork(index) {
|
|
|
15830
16040
|
saveConfig(stateDb, inferred, existing);
|
|
15831
16041
|
}
|
|
15832
16042
|
flywheelConfig = loadConfig(stateDb);
|
|
16043
|
+
const configKeys = Object.keys(flywheelConfig).filter((k) => flywheelConfig[k] != null);
|
|
16044
|
+
serverLog("config", `Config inferred: ${configKeys.join(", ")}`);
|
|
15833
16045
|
if (stateDb) {
|
|
15834
|
-
|
|
15835
|
-
|
|
16046
|
+
if (isTaskCacheStale()) {
|
|
16047
|
+
serverLog("tasks", "Task cache stale, rebuilding...");
|
|
16048
|
+
refreshIfStale(vaultPath, index, flywheelConfig.exclude_task_tags);
|
|
16049
|
+
} else {
|
|
16050
|
+
serverLog("tasks", "Task cache fresh, skipping rebuild");
|
|
16051
|
+
}
|
|
15836
16052
|
}
|
|
15837
16053
|
if (flywheelConfig.vault_name) {
|
|
15838
16054
|
serverLog("config", `Vault: ${flywheelConfig.vault_name}`);
|
|
@@ -15878,36 +16094,47 @@ async function runPostIndexWork(index) {
|
|
|
15878
16094
|
serverLog("watcher", `Processing ${batch.events.length} file changes`);
|
|
15879
16095
|
const batchStart = Date.now();
|
|
15880
16096
|
const changedPaths = batch.events.map((e) => e.path);
|
|
16097
|
+
const tracker = createStepTracker();
|
|
15881
16098
|
try {
|
|
16099
|
+
tracker.start("index_rebuild", { files_changed: batch.events.length, changed_paths: changedPaths });
|
|
15882
16100
|
vaultIndex = await buildVaultIndex(vaultPath);
|
|
15883
16101
|
setIndexState("ready");
|
|
15884
|
-
|
|
15885
|
-
serverLog("watcher", `Index rebuilt
|
|
15886
|
-
|
|
15887
|
-
recordIndexEvent(stateDb, {
|
|
15888
|
-
trigger: "watcher",
|
|
15889
|
-
duration_ms: duration,
|
|
15890
|
-
note_count: vaultIndex.notes.size,
|
|
15891
|
-
files_changed: batch.events.length,
|
|
15892
|
-
changed_paths: changedPaths
|
|
15893
|
-
});
|
|
15894
|
-
}
|
|
16102
|
+
tracker.end({ note_count: vaultIndex.notes.size, entity_count: vaultIndex.entities.size, tag_count: vaultIndex.tags.size });
|
|
16103
|
+
serverLog("watcher", `Index rebuilt: ${vaultIndex.notes.size} notes, ${vaultIndex.entities.size} entities`);
|
|
16104
|
+
tracker.start("entity_scan", { note_count: vaultIndex.notes.size });
|
|
15895
16105
|
await updateEntitiesInStateDb();
|
|
15896
|
-
|
|
16106
|
+
const entityCount = stateDb ? getAllEntitiesFromDb3(stateDb).length : 0;
|
|
16107
|
+
tracker.end({ entity_count: entityCount });
|
|
16108
|
+
serverLog("watcher", `Entity scan: ${entityCount} entities`);
|
|
16109
|
+
tracker.start("hub_scores", { entity_count: entityCount });
|
|
16110
|
+
const hubUpdated = await exportHubScores(vaultIndex, stateDb);
|
|
16111
|
+
tracker.end({ updated: hubUpdated ?? 0 });
|
|
16112
|
+
serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
|
|
15897
16113
|
if (hasEmbeddingsIndex()) {
|
|
16114
|
+
tracker.start("note_embeddings", { files: batch.events.length });
|
|
16115
|
+
let embUpdated = 0;
|
|
16116
|
+
let embRemoved = 0;
|
|
15898
16117
|
for (const event of batch.events) {
|
|
15899
16118
|
try {
|
|
15900
16119
|
if (event.type === "delete") {
|
|
15901
16120
|
removeEmbedding(event.path);
|
|
16121
|
+
embRemoved++;
|
|
15902
16122
|
} else if (event.path.endsWith(".md")) {
|
|
15903
16123
|
const absPath = path29.join(vaultPath, event.path);
|
|
15904
16124
|
await updateEmbedding(event.path, absPath);
|
|
16125
|
+
embUpdated++;
|
|
15905
16126
|
}
|
|
15906
16127
|
} catch {
|
|
15907
16128
|
}
|
|
15908
16129
|
}
|
|
16130
|
+
tracker.end({ updated: embUpdated, removed: embRemoved });
|
|
16131
|
+
serverLog("watcher", `Note embeddings: ${embUpdated} updated, ${embRemoved} removed`);
|
|
16132
|
+
} else {
|
|
16133
|
+
tracker.skip("note_embeddings", "not built");
|
|
15909
16134
|
}
|
|
15910
16135
|
if (hasEntityEmbeddingsIndex() && stateDb) {
|
|
16136
|
+
tracker.start("entity_embeddings", { files: batch.events.length });
|
|
16137
|
+
let entEmbUpdated = 0;
|
|
15911
16138
|
try {
|
|
15912
16139
|
const allEntities = getAllEntitiesFromDb3(stateDb);
|
|
15913
16140
|
for (const event of batch.events) {
|
|
@@ -15920,28 +16147,58 @@ async function runPostIndexWork(index) {
|
|
|
15920
16147
|
category: entity.category,
|
|
15921
16148
|
aliases: entity.aliases
|
|
15922
16149
|
}, vaultPath);
|
|
16150
|
+
entEmbUpdated++;
|
|
15923
16151
|
}
|
|
15924
16152
|
}
|
|
15925
16153
|
} catch {
|
|
15926
16154
|
}
|
|
16155
|
+
tracker.end({ updated: entEmbUpdated });
|
|
16156
|
+
serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated`);
|
|
16157
|
+
} else {
|
|
16158
|
+
tracker.skip("entity_embeddings", !stateDb ? "no stateDb" : "not built");
|
|
15927
16159
|
}
|
|
15928
16160
|
if (stateDb) {
|
|
16161
|
+
tracker.start("index_cache", { note_count: vaultIndex.notes.size });
|
|
15929
16162
|
try {
|
|
15930
16163
|
saveVaultIndexToCache(stateDb, vaultIndex);
|
|
16164
|
+
tracker.end({ saved: true });
|
|
16165
|
+
serverLog("watcher", "Index cache saved");
|
|
15931
16166
|
} catch (err) {
|
|
16167
|
+
tracker.end({ saved: false, error: err instanceof Error ? err.message : String(err) });
|
|
15932
16168
|
serverLog("index", `Failed to update index cache: ${err instanceof Error ? err.message : err}`, "error");
|
|
15933
16169
|
}
|
|
16170
|
+
} else {
|
|
16171
|
+
tracker.skip("index_cache", "no stateDb");
|
|
15934
16172
|
}
|
|
16173
|
+
tracker.start("task_cache", { files: batch.events.length });
|
|
16174
|
+
let taskUpdated = 0;
|
|
16175
|
+
let taskRemoved = 0;
|
|
15935
16176
|
for (const event of batch.events) {
|
|
15936
16177
|
try {
|
|
15937
16178
|
if (event.type === "delete") {
|
|
15938
16179
|
removeTaskCacheForFile(event.path);
|
|
16180
|
+
taskRemoved++;
|
|
15939
16181
|
} else if (event.path.endsWith(".md")) {
|
|
15940
16182
|
await updateTaskCacheForFile(vaultPath, event.path);
|
|
16183
|
+
taskUpdated++;
|
|
15941
16184
|
}
|
|
15942
16185
|
} catch {
|
|
15943
16186
|
}
|
|
15944
16187
|
}
|
|
16188
|
+
tracker.end({ updated: taskUpdated, removed: taskRemoved });
|
|
16189
|
+
serverLog("watcher", `Task cache: ${taskUpdated} updated, ${taskRemoved} removed`);
|
|
16190
|
+
const duration = Date.now() - batchStart;
|
|
16191
|
+
if (stateDb) {
|
|
16192
|
+
recordIndexEvent(stateDb, {
|
|
16193
|
+
trigger: "watcher",
|
|
16194
|
+
duration_ms: duration,
|
|
16195
|
+
note_count: vaultIndex.notes.size,
|
|
16196
|
+
files_changed: batch.events.length,
|
|
16197
|
+
changed_paths: changedPaths,
|
|
16198
|
+
steps: tracker.steps
|
|
16199
|
+
});
|
|
16200
|
+
}
|
|
16201
|
+
serverLog("watcher", `Batch complete: ${batch.events.length} files, ${duration}ms, ${tracker.steps.length} steps`);
|
|
15945
16202
|
} catch (err) {
|
|
15946
16203
|
setIndexState("error");
|
|
15947
16204
|
setIndexError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -15953,7 +16210,8 @@ async function runPostIndexWork(index) {
|
|
|
15953
16210
|
success: false,
|
|
15954
16211
|
files_changed: batch.events.length,
|
|
15955
16212
|
changed_paths: changedPaths,
|
|
15956
|
-
error: err instanceof Error ? err.message : String(err)
|
|
16213
|
+
error: err instanceof Error ? err.message : String(err),
|
|
16214
|
+
steps: tracker.steps
|
|
15957
16215
|
});
|
|
15958
16216
|
}
|
|
15959
16217
|
serverLog("watcher", `Failed to rebuild index: ${err instanceof Error ? err.message : err}`, "error");
|
|
@@ -15969,7 +16227,10 @@ async function runPostIndexWork(index) {
|
|
|
15969
16227
|
}
|
|
15970
16228
|
});
|
|
15971
16229
|
watcher.start();
|
|
16230
|
+
serverLog("watcher", "File watcher started");
|
|
15972
16231
|
}
|
|
16232
|
+
const postDuration = Date.now() - postStart;
|
|
16233
|
+
serverLog("server", `Post-index work complete in ${postDuration}ms`);
|
|
15973
16234
|
}
|
|
15974
16235
|
if (process.argv.includes("--init-semantic")) {
|
|
15975
16236
|
(async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.30",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
53
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
53
|
+
"@velvetmonkey/vault-core": "^2.0.30",
|
|
54
54
|
"better-sqlite3": "^11.0.0",
|
|
55
55
|
"chokidar": "^4.0.0",
|
|
56
56
|
"gray-matter": "^4.0.3",
|