@velvetmonkey/flywheel-memory 2.0.119 → 2.0.121
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 +531 -214
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -128,17 +128,23 @@ var init_levenshtein = __esm({
|
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
// src/vault-scope.ts
|
|
131
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
131
132
|
function getActiveScopeOrNull() {
|
|
132
|
-
return
|
|
133
|
+
return vaultAls.getStore() ?? fallbackScope;
|
|
133
134
|
}
|
|
134
|
-
function
|
|
135
|
-
|
|
135
|
+
function runInVaultScope(scope, fn) {
|
|
136
|
+
return vaultAls.run(scope, fn);
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
+
function setFallbackScope(scope) {
|
|
139
|
+
fallbackScope = scope;
|
|
140
|
+
}
|
|
141
|
+
var vaultAls, fallbackScope, setActiveScope;
|
|
138
142
|
var init_vault_scope = __esm({
|
|
139
143
|
"src/vault-scope.ts"() {
|
|
140
144
|
"use strict";
|
|
141
|
-
|
|
145
|
+
vaultAls = new AsyncLocalStorage();
|
|
146
|
+
fallbackScope = null;
|
|
147
|
+
setActiveScope = setFallbackScope;
|
|
142
148
|
}
|
|
143
149
|
});
|
|
144
150
|
|
|
@@ -155,14 +161,22 @@ function getModelConfig() {
|
|
|
155
161
|
function getActiveModelId() {
|
|
156
162
|
return activeModelConfig.id;
|
|
157
163
|
}
|
|
164
|
+
function getDb() {
|
|
165
|
+
return getActiveScopeOrNull()?.stateDb?.db ?? db;
|
|
166
|
+
}
|
|
167
|
+
function getEmbMap() {
|
|
168
|
+
return getActiveScopeOrNull()?.entityEmbeddingsMap ?? entityEmbeddingsMap;
|
|
169
|
+
}
|
|
158
170
|
function getEmbeddingsBuildState() {
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
const db4 = getDb();
|
|
172
|
+
if (!db4) return "none";
|
|
173
|
+
const row = db4.prepare(`SELECT value FROM fts_metadata WHERE key = 'embeddings_state'`).get();
|
|
161
174
|
return row?.value || "none";
|
|
162
175
|
}
|
|
163
176
|
function setEmbeddingsBuildState(state2) {
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
const db4 = getDb();
|
|
178
|
+
if (!db4) return;
|
|
179
|
+
db4.prepare(`INSERT OR REPLACE INTO fts_metadata (key, value) VALUES ('embeddings_state', ?)`).run(state2);
|
|
166
180
|
}
|
|
167
181
|
function setEmbeddingsDatabase(database) {
|
|
168
182
|
db = database;
|
|
@@ -273,7 +287,8 @@ function shouldIndexFile(filePath) {
|
|
|
273
287
|
return !parts.some((part) => EXCLUDED_DIRS2.has(part));
|
|
274
288
|
}
|
|
275
289
|
async function buildEmbeddingsIndex(vaultPath2, onProgress) {
|
|
276
|
-
|
|
290
|
+
const db4 = getDb();
|
|
291
|
+
if (!db4) {
|
|
277
292
|
throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
|
|
278
293
|
}
|
|
279
294
|
embeddingsBuilding = true;
|
|
@@ -282,11 +297,11 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
|
|
|
282
297
|
const files = await scanVault(vaultPath2);
|
|
283
298
|
const indexable = files.filter((f) => shouldIndexFile(f.path));
|
|
284
299
|
const existingHashes = /* @__PURE__ */ new Map();
|
|
285
|
-
const rows =
|
|
300
|
+
const rows = db4.prepare("SELECT path, content_hash FROM note_embeddings").all();
|
|
286
301
|
for (const row of rows) {
|
|
287
302
|
existingHashes.set(row.path, row.content_hash);
|
|
288
303
|
}
|
|
289
|
-
const upsert =
|
|
304
|
+
const upsert = db4.prepare(`
|
|
290
305
|
INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
|
|
291
306
|
VALUES (?, ?, ?, ?, ?)
|
|
292
307
|
`);
|
|
@@ -318,7 +333,7 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
|
|
|
318
333
|
if (onProgress) onProgress(progress);
|
|
319
334
|
}
|
|
320
335
|
const currentPaths = new Set(indexable.map((f) => f.path));
|
|
321
|
-
const deleteStmt =
|
|
336
|
+
const deleteStmt = db4.prepare("DELETE FROM note_embeddings WHERE path = ?");
|
|
322
337
|
for (const existingPath of existingHashes.keys()) {
|
|
323
338
|
if (!currentPaths.has(existingPath)) {
|
|
324
339
|
deleteStmt.run(existingPath);
|
|
@@ -329,15 +344,16 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
|
|
|
329
344
|
return progress;
|
|
330
345
|
}
|
|
331
346
|
async function updateEmbedding(notePath, absolutePath) {
|
|
332
|
-
|
|
347
|
+
const db4 = getDb();
|
|
348
|
+
if (!db4) return;
|
|
333
349
|
try {
|
|
334
350
|
const content = fs3.readFileSync(absolutePath, "utf-8");
|
|
335
351
|
const hash = contentHash(content);
|
|
336
|
-
const existing =
|
|
352
|
+
const existing = db4.prepare("SELECT content_hash FROM note_embeddings WHERE path = ?").get(notePath);
|
|
337
353
|
if (existing?.content_hash === hash) return;
|
|
338
354
|
const embedding = await embedText(content);
|
|
339
355
|
const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
340
|
-
|
|
356
|
+
db4.prepare(`
|
|
341
357
|
INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
|
|
342
358
|
VALUES (?, ?, ?, ?, ?)
|
|
343
359
|
`).run(notePath, buf, hash, activeModelConfig.id, Date.now());
|
|
@@ -345,8 +361,9 @@ async function updateEmbedding(notePath, absolutePath) {
|
|
|
345
361
|
}
|
|
346
362
|
}
|
|
347
363
|
function removeEmbedding(notePath) {
|
|
348
|
-
|
|
349
|
-
|
|
364
|
+
const db4 = getDb();
|
|
365
|
+
if (!db4) return;
|
|
366
|
+
db4.prepare("DELETE FROM note_embeddings WHERE path = ?").run(notePath);
|
|
350
367
|
}
|
|
351
368
|
function cosineSimilarity(a, b) {
|
|
352
369
|
let dot = 0;
|
|
@@ -362,11 +379,12 @@ function cosineSimilarity(a, b) {
|
|
|
362
379
|
return dot / denom;
|
|
363
380
|
}
|
|
364
381
|
async function semanticSearch(query, limit = 10) {
|
|
365
|
-
|
|
382
|
+
const db4 = getDb();
|
|
383
|
+
if (!db4) {
|
|
366
384
|
throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
|
|
367
385
|
}
|
|
368
386
|
const queryEmbedding = await embedText(query);
|
|
369
|
-
const rows =
|
|
387
|
+
const rows = db4.prepare("SELECT path, embedding FROM note_embeddings").all();
|
|
370
388
|
const scored = [];
|
|
371
389
|
for (const row of rows) {
|
|
372
390
|
const noteEmbedding = new Float32Array(
|
|
@@ -382,10 +400,11 @@ async function semanticSearch(query, limit = 10) {
|
|
|
382
400
|
return scored.slice(0, limit);
|
|
383
401
|
}
|
|
384
402
|
async function findSemanticallySimilar(sourcePath, limit = 10, excludePaths) {
|
|
385
|
-
|
|
403
|
+
const db4 = getDb();
|
|
404
|
+
if (!db4) {
|
|
386
405
|
throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
|
|
387
406
|
}
|
|
388
|
-
const sourceRow =
|
|
407
|
+
const sourceRow = db4.prepare("SELECT embedding FROM note_embeddings WHERE path = ?").get(sourcePath);
|
|
389
408
|
if (!sourceRow) {
|
|
390
409
|
return [];
|
|
391
410
|
}
|
|
@@ -394,7 +413,7 @@ async function findSemanticallySimilar(sourcePath, limit = 10, excludePaths) {
|
|
|
394
413
|
sourceRow.embedding.byteOffset,
|
|
395
414
|
sourceRow.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
|
|
396
415
|
);
|
|
397
|
-
const rows =
|
|
416
|
+
const rows = db4.prepare("SELECT path, embedding FROM note_embeddings WHERE path != ?").all(sourcePath);
|
|
398
417
|
const scored = [];
|
|
399
418
|
for (const row of rows) {
|
|
400
419
|
if (excludePaths?.has(row.path)) continue;
|
|
@@ -430,12 +449,13 @@ function setEmbeddingsBuilding(value) {
|
|
|
430
449
|
embeddingsBuilding = value;
|
|
431
450
|
}
|
|
432
451
|
function hasEmbeddingsIndex() {
|
|
433
|
-
|
|
452
|
+
const db4 = getDb();
|
|
453
|
+
if (!db4) return false;
|
|
434
454
|
try {
|
|
435
455
|
const state2 = getEmbeddingsBuildState();
|
|
436
456
|
if (state2 === "complete") return true;
|
|
437
457
|
if (state2 === "none") {
|
|
438
|
-
const row =
|
|
458
|
+
const row = db4.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
|
|
439
459
|
return row.count > 0;
|
|
440
460
|
}
|
|
441
461
|
return false;
|
|
@@ -444,9 +464,10 @@ function hasEmbeddingsIndex() {
|
|
|
444
464
|
}
|
|
445
465
|
}
|
|
446
466
|
function getStoredEmbeddingModel() {
|
|
447
|
-
|
|
467
|
+
const db4 = getDb();
|
|
468
|
+
if (!db4) return null;
|
|
448
469
|
try {
|
|
449
|
-
const row =
|
|
470
|
+
const row = db4.prepare("SELECT model FROM note_embeddings LIMIT 1").get();
|
|
450
471
|
return row?.model ?? null;
|
|
451
472
|
} catch {
|
|
452
473
|
return null;
|
|
@@ -458,19 +479,21 @@ function needsEmbeddingRebuild() {
|
|
|
458
479
|
return stored !== activeModelConfig.id;
|
|
459
480
|
}
|
|
460
481
|
function getEmbeddingsCount() {
|
|
461
|
-
|
|
482
|
+
const db4 = getDb();
|
|
483
|
+
if (!db4) return 0;
|
|
462
484
|
try {
|
|
463
|
-
const row =
|
|
485
|
+
const row = db4.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
|
|
464
486
|
return row.count;
|
|
465
487
|
} catch {
|
|
466
488
|
return 0;
|
|
467
489
|
}
|
|
468
490
|
}
|
|
469
491
|
function loadAllNoteEmbeddings() {
|
|
492
|
+
const db4 = getDb();
|
|
470
493
|
const result = /* @__PURE__ */ new Map();
|
|
471
|
-
if (!
|
|
494
|
+
if (!db4) return result;
|
|
472
495
|
try {
|
|
473
|
-
const rows =
|
|
496
|
+
const rows = db4.prepare("SELECT path, embedding FROM note_embeddings").all();
|
|
474
497
|
for (const row of rows) {
|
|
475
498
|
const embedding = new Float32Array(
|
|
476
499
|
row.embedding.buffer,
|
|
@@ -500,17 +523,18 @@ function buildEntityEmbeddingText(entity, vaultPath2) {
|
|
|
500
523
|
return parts.join(" ");
|
|
501
524
|
}
|
|
502
525
|
async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
|
|
503
|
-
|
|
526
|
+
const db4 = getDb();
|
|
527
|
+
if (!db4) {
|
|
504
528
|
throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
|
|
505
529
|
}
|
|
506
530
|
await initEmbeddings();
|
|
507
531
|
setEmbeddingsBuildState("building_entities");
|
|
508
532
|
const existingHashes = /* @__PURE__ */ new Map();
|
|
509
|
-
const rows =
|
|
533
|
+
const rows = db4.prepare("SELECT entity_name, source_hash FROM entity_embeddings").all();
|
|
510
534
|
for (const row of rows) {
|
|
511
535
|
existingHashes.set(row.entity_name, row.source_hash);
|
|
512
536
|
}
|
|
513
|
-
const upsert =
|
|
537
|
+
const upsert = db4.prepare(`
|
|
514
538
|
INSERT OR REPLACE INTO entity_embeddings (entity_name, embedding, source_hash, model, updated_at)
|
|
515
539
|
VALUES (?, ?, ?, ?, ?)
|
|
516
540
|
`);
|
|
@@ -534,7 +558,7 @@ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
|
|
|
534
558
|
}
|
|
535
559
|
if (onProgress) onProgress(done, total);
|
|
536
560
|
}
|
|
537
|
-
const deleteStmt =
|
|
561
|
+
const deleteStmt = db4.prepare("DELETE FROM entity_embeddings WHERE entity_name = ?");
|
|
538
562
|
for (const existingName of existingHashes.keys()) {
|
|
539
563
|
if (!entities.has(existingName)) {
|
|
540
564
|
deleteStmt.run(existingName);
|
|
@@ -544,15 +568,16 @@ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
|
|
|
544
568
|
return updated;
|
|
545
569
|
}
|
|
546
570
|
async function updateEntityEmbedding(entityName, entity, vaultPath2) {
|
|
547
|
-
|
|
571
|
+
const db4 = getDb();
|
|
572
|
+
if (!db4) return;
|
|
548
573
|
try {
|
|
549
574
|
const text = buildEntityEmbeddingText(entity, vaultPath2);
|
|
550
575
|
const hash = contentHash(text);
|
|
551
|
-
const existing =
|
|
576
|
+
const existing = db4.prepare("SELECT source_hash FROM entity_embeddings WHERE entity_name = ?").get(entityName);
|
|
552
577
|
if (existing?.source_hash === hash) return;
|
|
553
578
|
const embedding = await embedTextCached(text);
|
|
554
579
|
const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
555
|
-
|
|
580
|
+
db4.prepare(`
|
|
556
581
|
INSERT OR REPLACE INTO entity_embeddings (entity_name, embedding, source_hash, model, updated_at)
|
|
557
582
|
VALUES (?, ?, ?, ?, ?)
|
|
558
583
|
`).run(entityName, buf, hash, activeModelConfig.id, Date.now());
|
|
@@ -562,7 +587,7 @@ async function updateEntityEmbedding(entityName, entity, vaultPath2) {
|
|
|
562
587
|
}
|
|
563
588
|
function findSemanticallySimilarEntities(queryEmbedding, limit, excludeEntities) {
|
|
564
589
|
const scored = [];
|
|
565
|
-
for (const [entityName, embedding] of
|
|
590
|
+
for (const [entityName, embedding] of getEmbMap()) {
|
|
566
591
|
if (excludeEntities?.has(entityName)) continue;
|
|
567
592
|
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
568
593
|
scored.push({ entityName, similarity: Math.round(similarity * 1e3) / 1e3 });
|
|
@@ -571,12 +596,16 @@ function findSemanticallySimilarEntities(queryEmbedding, limit, excludeEntities)
|
|
|
571
596
|
return scored.slice(0, limit);
|
|
572
597
|
}
|
|
573
598
|
function hasEntityEmbeddingsIndex() {
|
|
574
|
-
return
|
|
599
|
+
return getEmbMap().size > 0;
|
|
600
|
+
}
|
|
601
|
+
function getEntityEmbeddingsMap() {
|
|
602
|
+
return entityEmbeddingsMap;
|
|
575
603
|
}
|
|
576
604
|
function loadEntityEmbeddingsToMemory() {
|
|
577
|
-
|
|
605
|
+
const db4 = getDb();
|
|
606
|
+
if (!db4) return;
|
|
578
607
|
try {
|
|
579
|
-
const rows =
|
|
608
|
+
const rows = db4.prepare("SELECT entity_name, embedding FROM entity_embeddings").all();
|
|
580
609
|
entityEmbeddingsMap.clear();
|
|
581
610
|
for (const row of rows) {
|
|
582
611
|
const embedding = new Float32Array(
|
|
@@ -593,10 +622,11 @@ function loadEntityEmbeddingsToMemory() {
|
|
|
593
622
|
}
|
|
594
623
|
}
|
|
595
624
|
function loadNoteEmbeddingsForPaths(paths) {
|
|
625
|
+
const db4 = getDb();
|
|
596
626
|
const result = /* @__PURE__ */ new Map();
|
|
597
|
-
if (!
|
|
627
|
+
if (!db4 || paths.length === 0) return result;
|
|
598
628
|
try {
|
|
599
|
-
const stmt =
|
|
629
|
+
const stmt = db4.prepare("SELECT path, embedding FROM note_embeddings WHERE path = ?");
|
|
600
630
|
for (const p of paths) {
|
|
601
631
|
const row = stmt.get(p);
|
|
602
632
|
if (row) {
|
|
@@ -613,12 +643,13 @@ function loadNoteEmbeddingsForPaths(paths) {
|
|
|
613
643
|
return result;
|
|
614
644
|
}
|
|
615
645
|
function getEntityEmbedding(entityName) {
|
|
616
|
-
return
|
|
646
|
+
return getEmbMap().get(entityName) ?? null;
|
|
617
647
|
}
|
|
618
648
|
function getEntityEmbeddingsCount() {
|
|
619
|
-
|
|
649
|
+
const db4 = getDb();
|
|
650
|
+
if (!db4) return 0;
|
|
620
651
|
try {
|
|
621
|
-
const row =
|
|
652
|
+
const row = db4.prepare("SELECT COUNT(*) as count FROM entity_embeddings").get();
|
|
622
653
|
return row.count;
|
|
623
654
|
} catch {
|
|
624
655
|
return 0;
|
|
@@ -1828,6 +1859,9 @@ import {
|
|
|
1828
1859
|
recordEntityMention,
|
|
1829
1860
|
getAllRecency as getAllRecencyFromDb
|
|
1830
1861
|
} from "@velvetmonkey/vault-core";
|
|
1862
|
+
function getStateDb() {
|
|
1863
|
+
return getActiveScopeOrNull()?.stateDb ?? moduleStateDb3;
|
|
1864
|
+
}
|
|
1831
1865
|
function setRecencyStateDb(stateDb2) {
|
|
1832
1866
|
moduleStateDb3 = stateDb2;
|
|
1833
1867
|
}
|
|
@@ -1898,9 +1932,10 @@ function getRecencyBoost(entityName, index) {
|
|
|
1898
1932
|
return 0;
|
|
1899
1933
|
}
|
|
1900
1934
|
function loadRecencyFromStateDb() {
|
|
1901
|
-
|
|
1935
|
+
const stateDb2 = getStateDb();
|
|
1936
|
+
if (!stateDb2) return null;
|
|
1902
1937
|
try {
|
|
1903
|
-
const rows = getAllRecencyFromDb(
|
|
1938
|
+
const rows = getAllRecencyFromDb(stateDb2);
|
|
1904
1939
|
if (rows.length === 0) return null;
|
|
1905
1940
|
const lastMentioned = /* @__PURE__ */ new Map();
|
|
1906
1941
|
let maxTime = 0;
|
|
@@ -1920,16 +1955,17 @@ function loadRecencyFromStateDb() {
|
|
|
1920
1955
|
}
|
|
1921
1956
|
}
|
|
1922
1957
|
function saveRecencyToStateDb(index) {
|
|
1923
|
-
|
|
1924
|
-
|
|
1958
|
+
const stateDb2 = getStateDb();
|
|
1959
|
+
if (!stateDb2) {
|
|
1960
|
+
console.error("[Flywheel] saveRecencyToStateDb: No StateDb available");
|
|
1925
1961
|
return;
|
|
1926
1962
|
}
|
|
1927
1963
|
console.error(`[Flywheel] saveRecencyToStateDb: Saving ${index.lastMentioned.size} entries...`);
|
|
1928
1964
|
try {
|
|
1929
1965
|
for (const [entityNameLower, timestamp] of index.lastMentioned) {
|
|
1930
|
-
recordEntityMention(
|
|
1966
|
+
recordEntityMention(stateDb2, entityNameLower, new Date(timestamp));
|
|
1931
1967
|
}
|
|
1932
|
-
const count =
|
|
1968
|
+
const count = stateDb2.db.prepare("SELECT COUNT(*) as cnt FROM recency").get();
|
|
1933
1969
|
console.error(`[Flywheel] Saved recency: ${index.lastMentioned.size} entries \u2192 ${count.cnt} rows in table`);
|
|
1934
1970
|
} catch (e) {
|
|
1935
1971
|
console.error("[Flywheel] Failed to save recency to StateDb:", e);
|
|
@@ -1939,6 +1975,7 @@ var moduleStateDb3, RECENCY_CACHE_VERSION, EXCLUDED_FOLDERS;
|
|
|
1939
1975
|
var init_recency = __esm({
|
|
1940
1976
|
"src/core/shared/recency.ts"() {
|
|
1941
1977
|
"use strict";
|
|
1978
|
+
init_vault_scope();
|
|
1942
1979
|
moduleStateDb3 = null;
|
|
1943
1980
|
RECENCY_CACHE_VERSION = 1;
|
|
1944
1981
|
EXCLUDED_FOLDERS = /* @__PURE__ */ new Set([
|
|
@@ -2958,6 +2995,114 @@ var init_cooccurrence = __esm({
|
|
|
2958
2995
|
}
|
|
2959
2996
|
});
|
|
2960
2997
|
|
|
2998
|
+
// src/core/shared/retrievalCooccurrence.ts
|
|
2999
|
+
function mineRetrievalCooccurrence(stateDb2) {
|
|
3000
|
+
const db4 = stateDb2.db;
|
|
3001
|
+
const lastRow = db4.prepare(
|
|
3002
|
+
`SELECT value FROM fts_metadata WHERE key = ?`
|
|
3003
|
+
).get(LAST_PROCESSED_KEY);
|
|
3004
|
+
const lastId = lastRow ? parseInt(lastRow.value, 10) : 0;
|
|
3005
|
+
const toolPlaceholders = Array.from(RETRIEVAL_TOOLS).map(() => "?").join(",");
|
|
3006
|
+
const rows = db4.prepare(`
|
|
3007
|
+
SELECT id, session_id, note_paths, timestamp
|
|
3008
|
+
FROM tool_invocations
|
|
3009
|
+
WHERE id > ? AND tool_name IN (${toolPlaceholders}) AND note_paths IS NOT NULL AND session_id IS NOT NULL
|
|
3010
|
+
ORDER BY id
|
|
3011
|
+
`).all(lastId, ...RETRIEVAL_TOOLS);
|
|
3012
|
+
if (rows.length === 0) return 0;
|
|
3013
|
+
const sessionNotes = /* @__PURE__ */ new Map();
|
|
3014
|
+
for (const row of rows) {
|
|
3015
|
+
let paths;
|
|
3016
|
+
try {
|
|
3017
|
+
paths = JSON.parse(row.note_paths);
|
|
3018
|
+
} catch {
|
|
3019
|
+
continue;
|
|
3020
|
+
}
|
|
3021
|
+
const existing = sessionNotes.get(row.session_id);
|
|
3022
|
+
if (existing) {
|
|
3023
|
+
for (const p of paths) existing.paths.add(p);
|
|
3024
|
+
existing.timestamp = Math.max(existing.timestamp, row.timestamp);
|
|
3025
|
+
} else {
|
|
3026
|
+
sessionNotes.set(row.session_id, {
|
|
3027
|
+
paths: new Set(paths),
|
|
3028
|
+
timestamp: row.timestamp
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
const insertStmt = db4.prepare(`
|
|
3033
|
+
INSERT OR IGNORE INTO retrieval_cooccurrence (note_a, note_b, session_id, timestamp, weight)
|
|
3034
|
+
VALUES (?, ?, ?, ?, ?)
|
|
3035
|
+
`);
|
|
3036
|
+
let inserted = 0;
|
|
3037
|
+
const maxId = rows[rows.length - 1].id;
|
|
3038
|
+
const insertAll = db4.transaction(() => {
|
|
3039
|
+
for (const [sessionId, { paths, timestamp }] of sessionNotes) {
|
|
3040
|
+
const filtered = Array.from(paths).filter((p) => !DAILY_NOTE_RE.test(p));
|
|
3041
|
+
if (filtered.length < 2) continue;
|
|
3042
|
+
const weight = 1 / Math.log(filtered.length);
|
|
3043
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
3044
|
+
for (let j = i + 1; j < filtered.length; j++) {
|
|
3045
|
+
const [a, b] = filtered[i] < filtered[j] ? [filtered[i], filtered[j]] : [filtered[j], filtered[i]];
|
|
3046
|
+
const result = insertStmt.run(a, b, sessionId, timestamp, weight);
|
|
3047
|
+
if (result.changes > 0) inserted++;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
db4.prepare(
|
|
3052
|
+
`INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)`
|
|
3053
|
+
).run(LAST_PROCESSED_KEY, String(maxId));
|
|
3054
|
+
});
|
|
3055
|
+
insertAll();
|
|
3056
|
+
return inserted;
|
|
3057
|
+
}
|
|
3058
|
+
function buildRetrievalBoostMap(seedNotePaths, stateDb2) {
|
|
3059
|
+
if (seedNotePaths.size === 0) return /* @__PURE__ */ new Map();
|
|
3060
|
+
const now = Date.now();
|
|
3061
|
+
const halfLifeMs = HALF_LIFE_DAYS * 24 * 60 * 60 * 1e3;
|
|
3062
|
+
const lambda = Math.LN2 / halfLifeMs;
|
|
3063
|
+
const boostMap = /* @__PURE__ */ new Map();
|
|
3064
|
+
for (const seedPath of seedNotePaths) {
|
|
3065
|
+
const rows = stateDb2.db.prepare(`
|
|
3066
|
+
SELECT note_a, note_b, timestamp, weight
|
|
3067
|
+
FROM retrieval_cooccurrence
|
|
3068
|
+
WHERE note_a = ? OR note_b = ?
|
|
3069
|
+
`).all(seedPath, seedPath);
|
|
3070
|
+
for (const row of rows) {
|
|
3071
|
+
const otherNote = row.note_a === seedPath ? row.note_b : row.note_a;
|
|
3072
|
+
if (seedNotePaths.has(otherNote)) continue;
|
|
3073
|
+
const age = now - row.timestamp;
|
|
3074
|
+
const decayFactor = Math.exp(-lambda * age);
|
|
3075
|
+
const w = row.weight * decayFactor;
|
|
3076
|
+
boostMap.set(otherNote, (boostMap.get(otherNote) || 0) + w);
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
return boostMap;
|
|
3080
|
+
}
|
|
3081
|
+
function getRetrievalBoost(entityPath, retrievalBoostMap) {
|
|
3082
|
+
if (!entityPath || retrievalBoostMap.size === 0) return 0;
|
|
3083
|
+
const weight = retrievalBoostMap.get(entityPath) || 0;
|
|
3084
|
+
if (weight <= 0) return 0;
|
|
3085
|
+
return Math.min(Math.round(weight * 3), MAX_RETRIEVAL_BOOST);
|
|
3086
|
+
}
|
|
3087
|
+
function pruneStaleRetrievalCooccurrence(stateDb2, maxAgeDays = 30) {
|
|
3088
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
3089
|
+
const result = stateDb2.db.prepare(
|
|
3090
|
+
"DELETE FROM retrieval_cooccurrence WHERE timestamp < ?"
|
|
3091
|
+
).run(cutoff);
|
|
3092
|
+
return result.changes;
|
|
3093
|
+
}
|
|
3094
|
+
var MAX_RETRIEVAL_BOOST, HALF_LIFE_DAYS, LAST_PROCESSED_KEY, DAILY_NOTE_RE, RETRIEVAL_TOOLS;
|
|
3095
|
+
var init_retrievalCooccurrence = __esm({
|
|
3096
|
+
"src/core/shared/retrievalCooccurrence.ts"() {
|
|
3097
|
+
"use strict";
|
|
3098
|
+
MAX_RETRIEVAL_BOOST = 6;
|
|
3099
|
+
HALF_LIFE_DAYS = 7;
|
|
3100
|
+
LAST_PROCESSED_KEY = "retrieval_cooc_last_id";
|
|
3101
|
+
DAILY_NOTE_RE = /\d{4}-\d{2}-\d{2}\.md$/;
|
|
3102
|
+
RETRIEVAL_TOOLS = /* @__PURE__ */ new Set(["recall", "search", "search_notes"]);
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
|
|
2961
3106
|
// src/core/write/edgeWeights.ts
|
|
2962
3107
|
function setEdgeWeightStateDb(stateDb2) {
|
|
2963
3108
|
moduleStateDb4 = stateDb2;
|
|
@@ -3179,7 +3324,7 @@ function setWriteStateDb(stateDb2) {
|
|
|
3179
3324
|
setRecencyStateDb(stateDb2);
|
|
3180
3325
|
}
|
|
3181
3326
|
function getWriteStateDb() {
|
|
3182
|
-
return moduleStateDb5;
|
|
3327
|
+
return getActiveScopeOrNull()?.stateDb ?? moduleStateDb5;
|
|
3183
3328
|
}
|
|
3184
3329
|
function setWikilinkConfig(config) {
|
|
3185
3330
|
moduleConfig = config;
|
|
@@ -3713,6 +3858,21 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
3713
3858
|
if (displayName) cooccurrenceSeeds.add(displayName);
|
|
3714
3859
|
}
|
|
3715
3860
|
}
|
|
3861
|
+
const stateDb2 = getWriteStateDb();
|
|
3862
|
+
let retrievalBoostMap = /* @__PURE__ */ new Map();
|
|
3863
|
+
if (!disabled.has("cooccurrence") && stateDb2 && cooccurrenceSeeds.size > 0) {
|
|
3864
|
+
const seedNotePaths = /* @__PURE__ */ new Set();
|
|
3865
|
+
for (const seedName of cooccurrenceSeeds) {
|
|
3866
|
+
const seedEntity = entitiesWithTypes.find((e) => e.entity.name === seedName);
|
|
3867
|
+
if (seedEntity?.entity.path) seedNotePaths.add(seedEntity.entity.path);
|
|
3868
|
+
}
|
|
3869
|
+
if (seedNotePaths.size > 0) {
|
|
3870
|
+
try {
|
|
3871
|
+
retrievalBoostMap = buildRetrievalBoostMap(seedNotePaths, stateDb2);
|
|
3872
|
+
} catch {
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3716
3876
|
if (!disabled.has("cooccurrence") && cooccurrenceIndex && cooccurrenceSeeds.size > 0) {
|
|
3717
3877
|
for (const { entity, category } of entitiesWithTypes) {
|
|
3718
3878
|
const entityName = entity.name;
|
|
@@ -3720,7 +3880,9 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
3720
3880
|
if (!disabled.has("length_filter") && entityName.length > MAX_ENTITY_LENGTH) continue;
|
|
3721
3881
|
if (!disabled.has("article_filter") && isLikelyArticleTitle(entityName)) continue;
|
|
3722
3882
|
if (linkedEntities.has(entityName.toLowerCase())) continue;
|
|
3723
|
-
const
|
|
3883
|
+
const contentCoocBoost = getCooccurrenceBoost(entityName, cooccurrenceSeeds, cooccurrenceIndex, recencyIndex);
|
|
3884
|
+
const retrievalCoocBoost = getRetrievalBoost(entity.path, retrievalBoostMap);
|
|
3885
|
+
const boost = Math.max(contentCoocBoost, retrievalCoocBoost);
|
|
3724
3886
|
if (boost > 0) {
|
|
3725
3887
|
const existing = scoredEntities.find((e) => e.name === entityName);
|
|
3726
3888
|
if (existing) {
|
|
@@ -4082,6 +4244,7 @@ var init_wikilinks = __esm({
|
|
|
4082
4244
|
init_recency();
|
|
4083
4245
|
init_stemmer();
|
|
4084
4246
|
init_cooccurrence();
|
|
4247
|
+
init_retrievalCooccurrence();
|
|
4085
4248
|
init_recency();
|
|
4086
4249
|
init_embeddings();
|
|
4087
4250
|
init_edgeWeights();
|
|
@@ -5938,7 +6101,7 @@ var init_taskHelpers = __esm({
|
|
|
5938
6101
|
|
|
5939
6102
|
// src/index.ts
|
|
5940
6103
|
import * as path32 from "path";
|
|
5941
|
-
import { readFileSync as readFileSync5, realpathSync } from "fs";
|
|
6104
|
+
import { readFileSync as readFileSync5, realpathSync, existsSync as existsSync3, statSync as statSync5 } from "fs";
|
|
5942
6105
|
import { fileURLToPath } from "url";
|
|
5943
6106
|
import { dirname as dirname6, join as join17 } from "path";
|
|
5944
6107
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -7505,6 +7668,7 @@ async function flushLogs() {
|
|
|
7505
7668
|
// src/core/read/fts5.ts
|
|
7506
7669
|
init_vault();
|
|
7507
7670
|
init_serverLog();
|
|
7671
|
+
init_vault_scope();
|
|
7508
7672
|
import * as fs7 from "fs";
|
|
7509
7673
|
var EXCLUDED_DIRS3 = /* @__PURE__ */ new Set([
|
|
7510
7674
|
".obsidian",
|
|
@@ -7526,6 +7690,9 @@ function splitFrontmatter(raw) {
|
|
|
7526
7690
|
return { frontmatter: values, body: raw.substring(end + 4) };
|
|
7527
7691
|
}
|
|
7528
7692
|
var db2 = null;
|
|
7693
|
+
function getDb2() {
|
|
7694
|
+
return getActiveScopeOrNull()?.stateDb?.db ?? db2;
|
|
7695
|
+
}
|
|
7529
7696
|
var state = {
|
|
7530
7697
|
ready: false,
|
|
7531
7698
|
building: false,
|
|
@@ -7558,10 +7725,11 @@ function shouldIndexFile2(filePath) {
|
|
|
7558
7725
|
return !parts.some((part) => EXCLUDED_DIRS3.has(part));
|
|
7559
7726
|
}
|
|
7560
7727
|
async function buildFTS5Index(vaultPath2) {
|
|
7728
|
+
const db4 = getDb2();
|
|
7561
7729
|
try {
|
|
7562
7730
|
state.error = null;
|
|
7563
7731
|
state.building = true;
|
|
7564
|
-
if (!
|
|
7732
|
+
if (!db4) {
|
|
7565
7733
|
throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
|
|
7566
7734
|
}
|
|
7567
7735
|
const files = await scanVault(vaultPath2);
|
|
@@ -7581,16 +7749,16 @@ async function buildFTS5Index(vaultPath2) {
|
|
|
7581
7749
|
serverLog("fts5", `Skipping ${file.path}: ${err}`, "warn");
|
|
7582
7750
|
}
|
|
7583
7751
|
}
|
|
7584
|
-
const insert =
|
|
7752
|
+
const insert = db4.prepare(
|
|
7585
7753
|
"INSERT INTO notes_fts (path, title, frontmatter, content) VALUES (?, ?, ?, ?)"
|
|
7586
7754
|
);
|
|
7587
7755
|
const now = /* @__PURE__ */ new Date();
|
|
7588
|
-
const swapAll =
|
|
7589
|
-
|
|
7756
|
+
const swapAll = db4.transaction(() => {
|
|
7757
|
+
db4.exec("DELETE FROM notes_fts");
|
|
7590
7758
|
for (const row of rows) {
|
|
7591
7759
|
insert.run(...row);
|
|
7592
7760
|
}
|
|
7593
|
-
|
|
7761
|
+
db4.prepare(
|
|
7594
7762
|
"INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
|
|
7595
7763
|
).run("last_built", now.toISOString());
|
|
7596
7764
|
});
|
|
@@ -7617,11 +7785,12 @@ async function buildFTS5Index(vaultPath2) {
|
|
|
7617
7785
|
}
|
|
7618
7786
|
}
|
|
7619
7787
|
function isIndexStale(_vaultPath) {
|
|
7620
|
-
|
|
7788
|
+
const db4 = getDb2();
|
|
7789
|
+
if (!db4) {
|
|
7621
7790
|
return true;
|
|
7622
7791
|
}
|
|
7623
7792
|
try {
|
|
7624
|
-
const row =
|
|
7793
|
+
const row = db4.prepare(
|
|
7625
7794
|
"SELECT value FROM fts_metadata WHERE key = ?"
|
|
7626
7795
|
).get("last_built");
|
|
7627
7796
|
if (!row) {
|
|
@@ -7634,12 +7803,19 @@ function isIndexStale(_vaultPath) {
|
|
|
7634
7803
|
return true;
|
|
7635
7804
|
}
|
|
7636
7805
|
}
|
|
7806
|
+
function sanitizeFTS5Query(query) {
|
|
7807
|
+
if (!query?.trim()) return "";
|
|
7808
|
+
return query.replace(/"/g, '""').replace(/[(){}[\]^~:\-]/g, " ").replace(/\s+/g, " ").trim();
|
|
7809
|
+
}
|
|
7637
7810
|
function searchFTS5(_vaultPath, query, limit = 10) {
|
|
7638
|
-
|
|
7811
|
+
const db4 = getDb2();
|
|
7812
|
+
if (!db4) {
|
|
7639
7813
|
throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
|
|
7640
7814
|
}
|
|
7815
|
+
const sanitized = sanitizeFTS5Query(query);
|
|
7816
|
+
if (!sanitized) return [];
|
|
7641
7817
|
try {
|
|
7642
|
-
const stmt =
|
|
7818
|
+
const stmt = db4.prepare(`
|
|
7643
7819
|
SELECT
|
|
7644
7820
|
path,
|
|
7645
7821
|
title,
|
|
@@ -7649,11 +7825,11 @@ function searchFTS5(_vaultPath, query, limit = 10) {
|
|
|
7649
7825
|
ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
|
|
7650
7826
|
LIMIT ?
|
|
7651
7827
|
`);
|
|
7652
|
-
const results = stmt.all(
|
|
7828
|
+
const results = stmt.all(sanitized, limit);
|
|
7653
7829
|
return results;
|
|
7654
7830
|
} catch (err) {
|
|
7655
|
-
if (err instanceof Error && err.message.includes("fts5:
|
|
7656
|
-
|
|
7831
|
+
if (err instanceof Error && err.message.includes("fts5:")) {
|
|
7832
|
+
return [];
|
|
7657
7833
|
}
|
|
7658
7834
|
throw err;
|
|
7659
7835
|
}
|
|
@@ -7662,9 +7838,10 @@ function getFTS5State() {
|
|
|
7662
7838
|
return { ...state };
|
|
7663
7839
|
}
|
|
7664
7840
|
function getContentPreview(notePath, maxChars = 300) {
|
|
7665
|
-
|
|
7841
|
+
const db4 = getDb2();
|
|
7842
|
+
if (!db4) return null;
|
|
7666
7843
|
try {
|
|
7667
|
-
const row =
|
|
7844
|
+
const row = db4.prepare(
|
|
7668
7845
|
"SELECT substr(content, 1, ?) as preview FROM notes_fts WHERE path = ?"
|
|
7669
7846
|
).get(maxChars + 50, notePath);
|
|
7670
7847
|
if (!row?.preview) return null;
|
|
@@ -7675,9 +7852,10 @@ function getContentPreview(notePath, maxChars = 300) {
|
|
|
7675
7852
|
}
|
|
7676
7853
|
}
|
|
7677
7854
|
function countFTS5Mentions(term) {
|
|
7678
|
-
|
|
7855
|
+
const db4 = getDb2();
|
|
7856
|
+
if (!db4) return 0;
|
|
7679
7857
|
try {
|
|
7680
|
-
const result =
|
|
7858
|
+
const result = db4.prepare(
|
|
7681
7859
|
"SELECT COUNT(*) as cnt FROM notes_fts WHERE content MATCH ?"
|
|
7682
7860
|
).get(`"${term}"`);
|
|
7683
7861
|
return result?.cnt ?? 0;
|
|
@@ -7828,7 +8006,11 @@ async function getTasksWithDueDates(index, vaultPath2, options = {}) {
|
|
|
7828
8006
|
|
|
7829
8007
|
// src/core/read/taskCache.ts
|
|
7830
8008
|
init_serverLog();
|
|
8009
|
+
init_vault_scope();
|
|
7831
8010
|
var db3 = null;
|
|
8011
|
+
function getDb3() {
|
|
8012
|
+
return getActiveScopeOrNull()?.stateDb?.db ?? db3;
|
|
8013
|
+
}
|
|
7832
8014
|
var TASK_CACHE_STALE_MS = 30 * 60 * 1e3;
|
|
7833
8015
|
var cacheReady = false;
|
|
7834
8016
|
var rebuildInProgress = false;
|
|
@@ -7851,7 +8033,8 @@ function isTaskCacheBuilding() {
|
|
|
7851
8033
|
return rebuildInProgress;
|
|
7852
8034
|
}
|
|
7853
8035
|
async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
7854
|
-
|
|
8036
|
+
const db4 = getDb3();
|
|
8037
|
+
if (!db4) {
|
|
7855
8038
|
throw new Error("Task cache database not initialized. Call setTaskCacheDatabase() first.");
|
|
7856
8039
|
}
|
|
7857
8040
|
if (rebuildInProgress) return;
|
|
@@ -7882,16 +8065,16 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
7882
8065
|
]);
|
|
7883
8066
|
}
|
|
7884
8067
|
}
|
|
7885
|
-
const insertStmt =
|
|
8068
|
+
const insertStmt = db4.prepare(`
|
|
7886
8069
|
INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
|
|
7887
8070
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
7888
8071
|
`);
|
|
7889
|
-
const swapAll =
|
|
7890
|
-
|
|
8072
|
+
const swapAll = db4.transaction(() => {
|
|
8073
|
+
db4.prepare("DELETE FROM tasks").run();
|
|
7891
8074
|
for (const row of allRows) {
|
|
7892
8075
|
insertStmt.run(...row);
|
|
7893
8076
|
}
|
|
7894
|
-
|
|
8077
|
+
db4.prepare(
|
|
7895
8078
|
"INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
|
|
7896
8079
|
).run("task_cache_built", (/* @__PURE__ */ new Date()).toISOString());
|
|
7897
8080
|
});
|
|
@@ -7904,16 +8087,17 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
|
|
|
7904
8087
|
}
|
|
7905
8088
|
}
|
|
7906
8089
|
async function updateTaskCacheForFile(vaultPath2, relativePath) {
|
|
7907
|
-
|
|
7908
|
-
|
|
8090
|
+
const db4 = getDb3();
|
|
8091
|
+
if (!db4) return;
|
|
8092
|
+
db4.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
|
|
7909
8093
|
const absolutePath = path12.join(vaultPath2, relativePath);
|
|
7910
8094
|
const tasks = await extractTasksFromNote(relativePath, absolutePath);
|
|
7911
8095
|
if (tasks.length > 0) {
|
|
7912
|
-
const insertStmt =
|
|
8096
|
+
const insertStmt = db4.prepare(`
|
|
7913
8097
|
INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
|
|
7914
8098
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
7915
8099
|
`);
|
|
7916
|
-
const insertBatch =
|
|
8100
|
+
const insertBatch = db4.transaction(() => {
|
|
7917
8101
|
for (const task of tasks) {
|
|
7918
8102
|
insertStmt.run(
|
|
7919
8103
|
task.path,
|
|
@@ -7931,11 +8115,13 @@ async function updateTaskCacheForFile(vaultPath2, relativePath) {
|
|
|
7931
8115
|
}
|
|
7932
8116
|
}
|
|
7933
8117
|
function removeTaskCacheForFile(relativePath) {
|
|
7934
|
-
|
|
7935
|
-
|
|
8118
|
+
const db4 = getDb3();
|
|
8119
|
+
if (!db4) return;
|
|
8120
|
+
db4.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
|
|
7936
8121
|
}
|
|
7937
8122
|
function queryTasksFromCache(options) {
|
|
7938
|
-
|
|
8123
|
+
const db4 = getDb3();
|
|
8124
|
+
if (!db4) {
|
|
7939
8125
|
throw new Error("Task cache database not initialized.");
|
|
7940
8126
|
}
|
|
7941
8127
|
const { status, folder, tag, excludeTags = [], has_due_date, limit, offset = 0 } = options;
|
|
@@ -7974,7 +8160,7 @@ function queryTasksFromCache(options) {
|
|
|
7974
8160
|
countParams.push(...excludeTags);
|
|
7975
8161
|
}
|
|
7976
8162
|
const countWhere = countConditions.length > 0 ? "WHERE " + countConditions.join(" AND ") : "";
|
|
7977
|
-
const countRows =
|
|
8163
|
+
const countRows = db4.prepare(
|
|
7978
8164
|
`SELECT status, COUNT(*) as cnt FROM tasks ${countWhere} GROUP BY status`
|
|
7979
8165
|
).all(...countParams);
|
|
7980
8166
|
let openCount = 0;
|
|
@@ -7999,7 +8185,7 @@ function queryTasksFromCache(options) {
|
|
|
7999
8185
|
limitClause = " LIMIT ? OFFSET ?";
|
|
8000
8186
|
queryParams.push(limit, offset);
|
|
8001
8187
|
}
|
|
8002
|
-
const rows =
|
|
8188
|
+
const rows = db4.prepare(
|
|
8003
8189
|
`SELECT path, line, text, status, raw, context, tags_json, due_date FROM tasks ${whereClause} ${orderBy}${limitClause}`
|
|
8004
8190
|
).all(...queryParams);
|
|
8005
8191
|
const tasks = rows.map((row) => ({
|
|
@@ -8021,9 +8207,10 @@ function queryTasksFromCache(options) {
|
|
|
8021
8207
|
};
|
|
8022
8208
|
}
|
|
8023
8209
|
function isTaskCacheStale() {
|
|
8024
|
-
|
|
8210
|
+
const db4 = getDb3();
|
|
8211
|
+
if (!db4) return true;
|
|
8025
8212
|
try {
|
|
8026
|
-
const row =
|
|
8213
|
+
const row = db4.prepare(
|
|
8027
8214
|
"SELECT value FROM fts_metadata WHERE key = ?"
|
|
8028
8215
|
).get("task_cache_built");
|
|
8029
8216
|
if (!row) return true;
|
|
@@ -8372,7 +8559,7 @@ var GetBacklinksOutputSchema = {
|
|
|
8372
8559
|
returned_count: z.coerce.number().describe("Number of backlinks returned (may be limited)"),
|
|
8373
8560
|
backlinks: z.array(BacklinkItemSchema).describe("List of backlinks")
|
|
8374
8561
|
};
|
|
8375
|
-
function registerGraphTools(server2, getIndex, getVaultPath,
|
|
8562
|
+
function registerGraphTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
8376
8563
|
server2.registerTool(
|
|
8377
8564
|
"get_backlinks",
|
|
8378
8565
|
{
|
|
@@ -8503,7 +8690,7 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
8503
8690
|
limit: z.number().default(20).describe("Maximum number of results to return")
|
|
8504
8691
|
},
|
|
8505
8692
|
async ({ path: notePath, min_weight, limit: requestedLimit }) => {
|
|
8506
|
-
const stateDb2 =
|
|
8693
|
+
const stateDb2 = getStateDb2?.();
|
|
8507
8694
|
if (!stateDb2) {
|
|
8508
8695
|
return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
|
|
8509
8696
|
}
|
|
@@ -8546,7 +8733,7 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
8546
8733
|
limit: z.number().default(20).describe("Maximum number of results to return")
|
|
8547
8734
|
},
|
|
8548
8735
|
async ({ path: notePath, limit: requestedLimit }) => {
|
|
8549
|
-
const stateDb2 =
|
|
8736
|
+
const stateDb2 = getStateDb2?.();
|
|
8550
8737
|
if (!stateDb2) {
|
|
8551
8738
|
return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
|
|
8552
8739
|
}
|
|
@@ -8667,7 +8854,7 @@ function findEntityMatches(text, entities) {
|
|
|
8667
8854
|
}
|
|
8668
8855
|
return matches.sort((a, b) => a.start - b.start);
|
|
8669
8856
|
}
|
|
8670
|
-
function registerWikilinkTools(server2, getIndex, getVaultPath,
|
|
8857
|
+
function registerWikilinkTools(server2, getIndex, getVaultPath, getStateDb2 = () => null) {
|
|
8671
8858
|
const SuggestionSchema = z2.object({
|
|
8672
8859
|
entity: z2.string().describe("The matched text in the input"),
|
|
8673
8860
|
start: z2.coerce.number().describe("Start position in text (0-indexed)"),
|
|
@@ -8809,7 +8996,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath, getStateDb = ()
|
|
|
8809
8996
|
strictness: "balanced"
|
|
8810
8997
|
});
|
|
8811
8998
|
if (scored.detailed) {
|
|
8812
|
-
const stateDb2 =
|
|
8999
|
+
const stateDb2 = getStateDb2();
|
|
8813
9000
|
if (stateDb2) {
|
|
8814
9001
|
try {
|
|
8815
9002
|
const weightedStats = getWeightedEntityStats(stateDb2);
|
|
@@ -9600,7 +9787,7 @@ function getSweepResults() {
|
|
|
9600
9787
|
init_wikilinkFeedback();
|
|
9601
9788
|
init_embeddings();
|
|
9602
9789
|
var STALE_THRESHOLD_SECONDS = 300;
|
|
9603
|
-
function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}),
|
|
9790
|
+
function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb2 = () => null, getWatcherStatus2 = () => null) {
|
|
9604
9791
|
const IndexProgressSchema = z3.object({
|
|
9605
9792
|
parsed: z3.coerce.number().describe("Number of files parsed so far"),
|
|
9606
9793
|
total: z3.coerce.number().describe("Total number of files to parse")
|
|
@@ -9721,6 +9908,21 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
9721
9908
|
vaultAccessible = false;
|
|
9722
9909
|
recommendations.push("Vault path is not accessible. Check PROJECT_PATH environment variable.");
|
|
9723
9910
|
}
|
|
9911
|
+
let dbIntegrityFailed = false;
|
|
9912
|
+
const stateDb2 = getStateDb2();
|
|
9913
|
+
if (stateDb2) {
|
|
9914
|
+
try {
|
|
9915
|
+
const result = stateDb2.db.pragma("quick_check");
|
|
9916
|
+
const ok = result.length === 1 && Object.values(result[0])[0] === "ok";
|
|
9917
|
+
if (!ok) {
|
|
9918
|
+
dbIntegrityFailed = true;
|
|
9919
|
+
recommendations.push(`Database integrity check failed: ${Object.values(result[0])[0] ?? "unknown error"}`);
|
|
9920
|
+
}
|
|
9921
|
+
} catch (err) {
|
|
9922
|
+
dbIntegrityFailed = true;
|
|
9923
|
+
recommendations.push(`Database integrity check error: ${err instanceof Error ? err.message : err}`);
|
|
9924
|
+
}
|
|
9925
|
+
}
|
|
9724
9926
|
const indexBuilt = indexState2 === "ready" && index !== void 0 && index.notes !== void 0;
|
|
9725
9927
|
const indexAge = indexBuilt && index.builtAt ? Math.floor((Date.now() - index.builtAt.getTime()) / 1e3) : -1;
|
|
9726
9928
|
const indexStale = indexBuilt && indexAge > STALE_THRESHOLD_SECONDS;
|
|
@@ -9744,7 +9946,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
9744
9946
|
recommendations.push("No notes found in vault. Is PROJECT_PATH pointing to a markdown vault?");
|
|
9745
9947
|
}
|
|
9746
9948
|
let status;
|
|
9747
|
-
if (!vaultAccessible || indexState2 === "error") {
|
|
9949
|
+
if (!vaultAccessible || indexState2 === "error" || dbIntegrityFailed) {
|
|
9748
9950
|
status = "unhealthy";
|
|
9749
9951
|
} else if (indexState2 === "building" || indexStale || recommendations.length > 0) {
|
|
9750
9952
|
status = "degraded";
|
|
@@ -9769,7 +9971,6 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
9769
9971
|
const config = getConfig2();
|
|
9770
9972
|
const configInfo = Object.keys(config).length > 0 ? config : void 0;
|
|
9771
9973
|
let lastRebuild;
|
|
9772
|
-
const stateDb2 = getStateDb();
|
|
9773
9974
|
if (stateDb2) {
|
|
9774
9975
|
try {
|
|
9775
9976
|
const events = getRecentIndexEvents(stateDb2, 1);
|
|
@@ -10071,7 +10272,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
10071
10272
|
const checks = [];
|
|
10072
10273
|
const index = getIndex();
|
|
10073
10274
|
const vaultPath2 = getVaultPath();
|
|
10074
|
-
const stateDb2 =
|
|
10275
|
+
const stateDb2 = getStateDb2();
|
|
10075
10276
|
const watcherStatus = getWatcherStatus2();
|
|
10076
10277
|
checks.push({
|
|
10077
10278
|
name: "schema_version",
|
|
@@ -10517,6 +10718,7 @@ function enrichNoteResult(notePath, stateDb2, index) {
|
|
|
10517
10718
|
}
|
|
10518
10719
|
|
|
10519
10720
|
// src/tools/read/query.ts
|
|
10721
|
+
init_wikilinkFeedback();
|
|
10520
10722
|
function matchesFrontmatter(note, where) {
|
|
10521
10723
|
for (const [key, value] of Object.entries(where)) {
|
|
10522
10724
|
const noteValue = note.frontmatter[key];
|
|
@@ -10584,7 +10786,7 @@ function sortNotes(notes, sortBy, order) {
|
|
|
10584
10786
|
});
|
|
10585
10787
|
return sorted;
|
|
10586
10788
|
}
|
|
10587
|
-
function registerQueryTools(server2, getIndex, getVaultPath,
|
|
10789
|
+
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
10588
10790
|
server2.tool(
|
|
10589
10791
|
"search",
|
|
10590
10792
|
'Search the vault \u2014 always start with just a query, no filters. Top results get full metadata (frontmatter, top backlinks/outlinks ranked by edge weight + recency); remaining results get lightweight summaries. Narrow with filters only if the broad search returns too many irrelevant results. Use get_note_structure for headings/full structure, get_backlinks for complete backlink lists.\n\nSearches across content (FTS5 full-text + hybrid semantic), entities (people/projects/technologies), and metadata (frontmatter/tags/folders). Hybrid semantic results are automatically included when embeddings have been built (via init_semantic).\n\nExample: search({ query: "quarterly review", limit: 5 })\nExample: search({ where: { type: "project", status: "active" } })\n\nMulti-vault: when configured with multiple vaults, omitting the `vault` parameter searches all vaults and merges results (each result includes a `vault` field). Pass `vault` to search a specific vault.',
|
|
@@ -10618,7 +10820,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10618
10820
|
const index = getIndex();
|
|
10619
10821
|
const vaultPath2 = getVaultPath();
|
|
10620
10822
|
if (prefix && query) {
|
|
10621
|
-
const stateDb2 =
|
|
10823
|
+
const stateDb2 = getStateDb2();
|
|
10622
10824
|
if (!stateDb2) {
|
|
10623
10825
|
return { content: [{ type: "text", text: JSON.stringify({ results: [], count: 0, query, error: "StateDb not initialized" }, null, 2) }] };
|
|
10624
10826
|
}
|
|
@@ -10666,7 +10868,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10666
10868
|
matchingNotes = sortNotes(matchingNotes, sort_by ?? "modified", order ?? "desc");
|
|
10667
10869
|
const totalMatches = matchingNotes.length;
|
|
10668
10870
|
const limitedNotes = matchingNotes.slice(0, limit);
|
|
10669
|
-
const stateDb2 =
|
|
10871
|
+
const stateDb2 = getStateDb2();
|
|
10670
10872
|
const notes = limitedNotes.map(
|
|
10671
10873
|
(note, i) => (i < detailN ? enrichResult : enrichResultLight)({ path: note.path, title: note.title }, index, stateDb2)
|
|
10672
10874
|
);
|
|
@@ -10694,7 +10896,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10694
10896
|
}
|
|
10695
10897
|
const fts5Results = searchFTS5(vaultPath2, query, limit);
|
|
10696
10898
|
let entityResults = [];
|
|
10697
|
-
const stateDbEntity =
|
|
10899
|
+
const stateDbEntity = getStateDb2();
|
|
10698
10900
|
if (stateDbEntity) {
|
|
10699
10901
|
try {
|
|
10700
10902
|
entityResults = searchEntities(stateDbEntity, query, limit);
|
|
@@ -10703,7 +10905,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10703
10905
|
}
|
|
10704
10906
|
let edgeRanked = [];
|
|
10705
10907
|
if (context_note) {
|
|
10706
|
-
const ctxStateDb =
|
|
10908
|
+
const ctxStateDb = getStateDb2();
|
|
10707
10909
|
if (ctxStateDb) {
|
|
10708
10910
|
try {
|
|
10709
10911
|
const edgeRows = ctxStateDb.db.prepare(`
|
|
@@ -10768,7 +10970,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10768
10970
|
}));
|
|
10769
10971
|
scored.sort((a, b) => b.rrf_score - a.rrf_score);
|
|
10770
10972
|
const filtered = applyFolderFilter(scored);
|
|
10771
|
-
const stateDb2 =
|
|
10973
|
+
const stateDb2 = getStateDb2();
|
|
10772
10974
|
const results = filtered.slice(0, limit).map((item, i) => ({
|
|
10773
10975
|
...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
|
|
10774
10976
|
rrf_score: item.rrf_score,
|
|
@@ -10776,6 +10978,34 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10776
10978
|
in_semantic: item.in_semantic,
|
|
10777
10979
|
in_entity: item.in_entity
|
|
10778
10980
|
}));
|
|
10981
|
+
if (stateDb2 && results.length < limit) {
|
|
10982
|
+
const existingPaths = new Set(results.map((r) => r.path));
|
|
10983
|
+
const backfill = [];
|
|
10984
|
+
for (const r of results.slice(0, 3)) {
|
|
10985
|
+
const rPath = r.path;
|
|
10986
|
+
if (!rPath) continue;
|
|
10987
|
+
try {
|
|
10988
|
+
const outlinks = getStoredNoteLinks(stateDb2, rPath);
|
|
10989
|
+
for (const target of outlinks) {
|
|
10990
|
+
const entityRow = stateDb2.db.prepare(
|
|
10991
|
+
"SELECT path FROM entities WHERE name_lower = ?"
|
|
10992
|
+
).get(target);
|
|
10993
|
+
if (entityRow?.path && !existingPaths.has(entityRow.path)) {
|
|
10994
|
+
existingPaths.add(entityRow.path);
|
|
10995
|
+
backfill.push({
|
|
10996
|
+
...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2),
|
|
10997
|
+
rrf_score: 0,
|
|
10998
|
+
in_fts5: false,
|
|
10999
|
+
in_semantic: false,
|
|
11000
|
+
in_entity: false
|
|
11001
|
+
});
|
|
11002
|
+
}
|
|
11003
|
+
}
|
|
11004
|
+
} catch {
|
|
11005
|
+
}
|
|
11006
|
+
}
|
|
11007
|
+
results.push(...backfill.slice(0, limit - results.length));
|
|
11008
|
+
}
|
|
10779
11009
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
10780
11010
|
method: "hybrid",
|
|
10781
11011
|
query,
|
|
@@ -10795,12 +11025,36 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10795
11025
|
...entityRanked.map((r) => ({ path: r.path, title: r.name, snippet: void 0, in_entity: true }))
|
|
10796
11026
|
];
|
|
10797
11027
|
const filtered = applyFolderFilter(mergedItems);
|
|
10798
|
-
const stateDb2 =
|
|
11028
|
+
const stateDb2 = getStateDb2();
|
|
10799
11029
|
const sliced = filtered.slice(0, limit);
|
|
10800
11030
|
const results = sliced.map((item, i) => ({
|
|
10801
11031
|
...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
|
|
10802
11032
|
..."in_fts5" in item ? { in_fts5: true } : { in_entity: true }
|
|
10803
11033
|
}));
|
|
11034
|
+
if (stateDb2 && results.length < limit) {
|
|
11035
|
+
const existingPaths = new Set(results.map((r) => r.path));
|
|
11036
|
+
const backfill = [];
|
|
11037
|
+
for (const r of results.slice(0, 3)) {
|
|
11038
|
+
const rPath = r.path;
|
|
11039
|
+
if (!rPath) continue;
|
|
11040
|
+
try {
|
|
11041
|
+
const outlinks = getStoredNoteLinks(stateDb2, rPath);
|
|
11042
|
+
for (const target of outlinks) {
|
|
11043
|
+
const entityRow = stateDb2.db.prepare(
|
|
11044
|
+
"SELECT path FROM entities WHERE name_lower = ?"
|
|
11045
|
+
).get(target);
|
|
11046
|
+
if (entityRow?.path && !existingPaths.has(entityRow.path)) {
|
|
11047
|
+
existingPaths.add(entityRow.path);
|
|
11048
|
+
backfill.push({
|
|
11049
|
+
...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2)
|
|
11050
|
+
});
|
|
11051
|
+
}
|
|
11052
|
+
}
|
|
11053
|
+
} catch {
|
|
11054
|
+
}
|
|
11055
|
+
}
|
|
11056
|
+
results.push(...backfill.slice(0, limit - results.length));
|
|
11057
|
+
}
|
|
10804
11058
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
10805
11059
|
method: "fts5",
|
|
10806
11060
|
query,
|
|
@@ -10808,7 +11062,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10808
11062
|
results
|
|
10809
11063
|
}, null, 2) }] };
|
|
10810
11064
|
}
|
|
10811
|
-
const stateDbFts =
|
|
11065
|
+
const stateDbFts = getStateDb2();
|
|
10812
11066
|
const fts5Filtered = applyFolderFilter(fts5Results);
|
|
10813
11067
|
const enrichedFts5 = fts5Filtered.map((r, i) => ({ ...(i < detailN ? enrichResult : enrichResultLight)({ path: r.path, title: r.title, snippet: r.snippet }, index, stateDbFts), in_fts5: true }));
|
|
10814
11068
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
@@ -10896,7 +11150,7 @@ function suggestEntityAliases(stateDb2, folder) {
|
|
|
10896
11150
|
|
|
10897
11151
|
// src/tools/read/system.ts
|
|
10898
11152
|
init_edgeWeights();
|
|
10899
|
-
function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig,
|
|
11153
|
+
function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb2) {
|
|
10900
11154
|
const RefreshIndexOutputSchema = {
|
|
10901
11155
|
success: z5.boolean().describe("Whether the refresh succeeded"),
|
|
10902
11156
|
notes_count: z5.number().describe("Number of notes indexed"),
|
|
@@ -10922,7 +11176,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
10922
11176
|
const newIndex = await buildVaultIndex(vaultPath2);
|
|
10923
11177
|
setIndex(newIndex);
|
|
10924
11178
|
setIndexState("ready");
|
|
10925
|
-
const stateDb2 =
|
|
11179
|
+
const stateDb2 = getStateDb2?.();
|
|
10926
11180
|
if (stateDb2) {
|
|
10927
11181
|
try {
|
|
10928
11182
|
const entityIndex2 = await scanVaultEntities2(vaultPath2, {
|
|
@@ -11009,7 +11263,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
11009
11263
|
setIndexState("error");
|
|
11010
11264
|
setIndexError(err instanceof Error ? err : new Error(String(err)));
|
|
11011
11265
|
const duration = Date.now() - startTime;
|
|
11012
|
-
const stateDb2 =
|
|
11266
|
+
const stateDb2 = getStateDb2?.();
|
|
11013
11267
|
if (stateDb2) {
|
|
11014
11268
|
recordIndexEvent(stateDb2, {
|
|
11015
11269
|
trigger: "manual_refresh",
|
|
@@ -11267,7 +11521,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
11267
11521
|
category,
|
|
11268
11522
|
limit: perCategoryLimit
|
|
11269
11523
|
}) => {
|
|
11270
|
-
const stateDb2 =
|
|
11524
|
+
const stateDb2 = getStateDb2?.();
|
|
11271
11525
|
if (!stateDb2) {
|
|
11272
11526
|
return {
|
|
11273
11527
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -11322,7 +11576,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
11322
11576
|
folder,
|
|
11323
11577
|
limit: requestedLimit
|
|
11324
11578
|
}) => {
|
|
11325
|
-
const stateDb2 =
|
|
11579
|
+
const stateDb2 = getStateDb2?.();
|
|
11326
11580
|
if (!stateDb2) {
|
|
11327
11581
|
return {
|
|
11328
11582
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -11542,7 +11796,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
11542
11796
|
|
|
11543
11797
|
// src/tools/read/primitives.ts
|
|
11544
11798
|
import { getEntityByName as getEntityByName3 } from "@velvetmonkey/vault-core";
|
|
11545
|
-
function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}),
|
|
11799
|
+
function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb2 = () => null) {
|
|
11546
11800
|
server2.registerTool(
|
|
11547
11801
|
"get_note_structure",
|
|
11548
11802
|
{
|
|
@@ -11581,7 +11835,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
|
|
|
11581
11835
|
enriched.backlink_count = backlinks.length;
|
|
11582
11836
|
enriched.outlink_count = note.outlinks.length;
|
|
11583
11837
|
}
|
|
11584
|
-
const stateDb2 =
|
|
11838
|
+
const stateDb2 = getStateDb2();
|
|
11585
11839
|
if (stateDb2 && note) {
|
|
11586
11840
|
try {
|
|
11587
11841
|
const entity = getEntityByName3(stateDb2, note.title);
|
|
@@ -12640,7 +12894,7 @@ function getExcludedPaths(index, config) {
|
|
|
12640
12894
|
}
|
|
12641
12895
|
return excluded;
|
|
12642
12896
|
}
|
|
12643
|
-
function registerGraphAnalysisTools(server2, getIndex, getVaultPath,
|
|
12897
|
+
function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb2, getConfig2) {
|
|
12644
12898
|
server2.registerTool(
|
|
12645
12899
|
"graph_analysis",
|
|
12646
12900
|
{
|
|
@@ -12810,7 +13064,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
12810
13064
|
};
|
|
12811
13065
|
}
|
|
12812
13066
|
case "emerging_hubs": {
|
|
12813
|
-
const db4 =
|
|
13067
|
+
const db4 = getStateDb2?.();
|
|
12814
13068
|
if (!db4) {
|
|
12815
13069
|
return {
|
|
12816
13070
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -17558,7 +17812,7 @@ function registerTagTools(server2, getIndex, getVaultPath) {
|
|
|
17558
17812
|
// src/tools/write/wikilinkFeedback.ts
|
|
17559
17813
|
init_wikilinkFeedback();
|
|
17560
17814
|
import { z as z22 } from "zod";
|
|
17561
|
-
function registerWikilinkFeedbackTools(server2,
|
|
17815
|
+
function registerWikilinkFeedbackTools(server2, getStateDb2) {
|
|
17562
17816
|
server2.registerTool(
|
|
17563
17817
|
"wikilink_feedback",
|
|
17564
17818
|
{
|
|
@@ -17579,7 +17833,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
17579
17833
|
}
|
|
17580
17834
|
},
|
|
17581
17835
|
async ({ mode, entity, note_path, context, correct, limit, days_back, granularity, timestamp_before, timestamp_after, skip_status_update }) => {
|
|
17582
|
-
const stateDb2 =
|
|
17836
|
+
const stateDb2 = getStateDb2();
|
|
17583
17837
|
if (!stateDb2) {
|
|
17584
17838
|
return {
|
|
17585
17839
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available \u2014 database not initialized yet" }) }],
|
|
@@ -17741,7 +17995,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
17741
17995
|
// src/tools/write/corrections.ts
|
|
17742
17996
|
init_corrections();
|
|
17743
17997
|
import { z as z23 } from "zod";
|
|
17744
|
-
function registerCorrectionTools(server2,
|
|
17998
|
+
function registerCorrectionTools(server2, getStateDb2) {
|
|
17745
17999
|
server2.tool(
|
|
17746
18000
|
"vault_record_correction",
|
|
17747
18001
|
'Record a persistent correction (e.g., "that link is wrong", "undo that"). Survives across sessions.',
|
|
@@ -17752,7 +18006,7 @@ function registerCorrectionTools(server2, getStateDb) {
|
|
|
17752
18006
|
note_path: z23.string().optional().describe("Note path (if correction is about a specific note)")
|
|
17753
18007
|
},
|
|
17754
18008
|
async ({ correction_type, description, entity, note_path }) => {
|
|
17755
|
-
const stateDb2 =
|
|
18009
|
+
const stateDb2 = getStateDb2();
|
|
17756
18010
|
if (!stateDb2) {
|
|
17757
18011
|
return {
|
|
17758
18012
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -17780,7 +18034,7 @@ function registerCorrectionTools(server2, getStateDb) {
|
|
|
17780
18034
|
limit: z23.number().min(1).max(200).default(50).describe("Max entries to return")
|
|
17781
18035
|
},
|
|
17782
18036
|
async ({ status, entity, limit }) => {
|
|
17783
|
-
const stateDb2 =
|
|
18037
|
+
const stateDb2 = getStateDb2();
|
|
17784
18038
|
if (!stateDb2) {
|
|
17785
18039
|
return {
|
|
17786
18040
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -17807,7 +18061,7 @@ function registerCorrectionTools(server2, getStateDb) {
|
|
|
17807
18061
|
status: z23.enum(["applied", "dismissed"]).describe("New status")
|
|
17808
18062
|
},
|
|
17809
18063
|
async ({ correction_id, status }) => {
|
|
17810
|
-
const stateDb2 =
|
|
18064
|
+
const stateDb2 = getStateDb2();
|
|
17811
18065
|
if (!stateDb2) {
|
|
17812
18066
|
return {
|
|
17813
18067
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -18155,7 +18409,7 @@ function findContradictions2(stateDb2, entity) {
|
|
|
18155
18409
|
}
|
|
18156
18410
|
|
|
18157
18411
|
// src/tools/write/memory.ts
|
|
18158
|
-
function registerMemoryTools(server2,
|
|
18412
|
+
function registerMemoryTools(server2, getStateDb2) {
|
|
18159
18413
|
server2.tool(
|
|
18160
18414
|
"memory",
|
|
18161
18415
|
"Store, retrieve, search, and manage agent working memory. Actions: store, get, search, list, forget, summarize_session.",
|
|
@@ -18180,7 +18434,7 @@ function registerMemoryTools(server2, getStateDb) {
|
|
|
18180
18434
|
tool_count: z24.number().optional().describe("Number of tool calls in session")
|
|
18181
18435
|
},
|
|
18182
18436
|
async (args) => {
|
|
18183
|
-
const stateDb2 =
|
|
18437
|
+
const stateDb2 = getStateDb2();
|
|
18184
18438
|
if (!stateDb2) {
|
|
18185
18439
|
return {
|
|
18186
18440
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -18771,7 +19025,7 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
18771
19025
|
}
|
|
18772
19026
|
return selected;
|
|
18773
19027
|
}
|
|
18774
|
-
function registerRecallTools(server2,
|
|
19028
|
+
function registerRecallTools(server2, getStateDb2, getVaultPath, getIndex) {
|
|
18775
19029
|
server2.tool(
|
|
18776
19030
|
"recall",
|
|
18777
19031
|
"Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
|
|
@@ -18784,7 +19038,7 @@ function registerRecallTools(server2, getStateDb, getVaultPath, getIndex) {
|
|
|
18784
19038
|
diversity: z25.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
|
|
18785
19039
|
},
|
|
18786
19040
|
async (args) => {
|
|
18787
|
-
const stateDb2 =
|
|
19041
|
+
const stateDb2 = getStateDb2();
|
|
18788
19042
|
if (!stateDb2) {
|
|
18789
19043
|
return {
|
|
18790
19044
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -18840,15 +19094,17 @@ import { z as z26 } from "zod";
|
|
|
18840
19094
|
// src/core/shared/toolTracking.ts
|
|
18841
19095
|
function recordToolInvocation(stateDb2, event) {
|
|
18842
19096
|
stateDb2.db.prepare(
|
|
18843
|
-
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
|
|
18844
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
19097
|
+
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success, response_tokens, baseline_tokens)
|
|
19098
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
18845
19099
|
).run(
|
|
18846
19100
|
Date.now(),
|
|
18847
19101
|
event.tool_name,
|
|
18848
19102
|
event.session_id ?? null,
|
|
18849
19103
|
event.note_paths ? JSON.stringify(event.note_paths) : null,
|
|
18850
19104
|
event.duration_ms ?? null,
|
|
18851
|
-
event.success !== false ? 1 : 0
|
|
19105
|
+
event.success !== false ? 1 : 0,
|
|
19106
|
+
event.response_tokens ?? null,
|
|
19107
|
+
event.baseline_tokens ?? null
|
|
18852
19108
|
);
|
|
18853
19109
|
}
|
|
18854
19110
|
function rowToInvocation(row) {
|
|
@@ -18859,7 +19115,9 @@ function rowToInvocation(row) {
|
|
|
18859
19115
|
session_id: row.session_id,
|
|
18860
19116
|
note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
|
|
18861
19117
|
duration_ms: row.duration_ms,
|
|
18862
|
-
success: row.success === 1
|
|
19118
|
+
success: row.success === 1,
|
|
19119
|
+
response_tokens: row.response_tokens,
|
|
19120
|
+
baseline_tokens: row.baseline_tokens
|
|
18863
19121
|
};
|
|
18864
19122
|
}
|
|
18865
19123
|
function getToolUsageSummary(stateDb2, daysBack = 30) {
|
|
@@ -19119,7 +19377,7 @@ function buildVaultPulseSection(stateDb2) {
|
|
|
19119
19377
|
estimated_tokens: estimateTokens2(content)
|
|
19120
19378
|
};
|
|
19121
19379
|
}
|
|
19122
|
-
function registerBriefTools(server2,
|
|
19380
|
+
function registerBriefTools(server2, getStateDb2) {
|
|
19123
19381
|
server2.tool(
|
|
19124
19382
|
"brief",
|
|
19125
19383
|
"Get a startup context briefing: recent sessions, active entities, memories, pending corrections, and vault stats. Call at conversation start.",
|
|
@@ -19128,7 +19386,7 @@ function registerBriefTools(server2, getStateDb) {
|
|
|
19128
19386
|
focus: z26.string().optional().describe("Focus entity or topic (filters content)")
|
|
19129
19387
|
},
|
|
19130
19388
|
async (args) => {
|
|
19131
|
-
const stateDb2 =
|
|
19389
|
+
const stateDb2 = getStateDb2();
|
|
19132
19390
|
if (!stateDb2) {
|
|
19133
19391
|
return {
|
|
19134
19392
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -19189,7 +19447,7 @@ var VALID_CONFIG_KEYS = {
|
|
|
19189
19447
|
implicit_patterns: z27.array(z27.string()),
|
|
19190
19448
|
adaptive_strictness: z27.boolean()
|
|
19191
19449
|
};
|
|
19192
|
-
function registerConfigTools(server2, getConfig2, setConfig,
|
|
19450
|
+
function registerConfigTools(server2, getConfig2, setConfig, getStateDb2) {
|
|
19193
19451
|
server2.registerTool(
|
|
19194
19452
|
"flywheel_config",
|
|
19195
19453
|
{
|
|
@@ -19231,7 +19489,7 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb) {
|
|
|
19231
19489
|
}) }]
|
|
19232
19490
|
};
|
|
19233
19491
|
}
|
|
19234
|
-
const stateDb2 =
|
|
19492
|
+
const stateDb2 = getStateDb2();
|
|
19235
19493
|
if (!stateDb2) {
|
|
19236
19494
|
return {
|
|
19237
19495
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -19553,7 +19811,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
|
|
|
19553
19811
|
total_enriched: currentTotal
|
|
19554
19812
|
};
|
|
19555
19813
|
}
|
|
19556
|
-
function registerInitTools(server2, getVaultPath,
|
|
19814
|
+
function registerInitTools(server2, getVaultPath, getStateDb2) {
|
|
19557
19815
|
server2.tool(
|
|
19558
19816
|
"vault_init",
|
|
19559
19817
|
`Initialize vault for Flywheel. Modes: "status" (check what's ready/missing), "run" (execute missing init steps), "enrich" (scan notes with zero wikilinks and apply entity links).`,
|
|
@@ -19564,7 +19822,7 @@ function registerInitTools(server2, getVaultPath, getStateDb) {
|
|
|
19564
19822
|
offset: z28.number().default(0).describe("For enrich mode: skip this many eligible notes (for pagination)")
|
|
19565
19823
|
},
|
|
19566
19824
|
async ({ mode, dry_run, batch_size, offset }) => {
|
|
19567
|
-
const stateDb2 =
|
|
19825
|
+
const stateDb2 = getStateDb2();
|
|
19568
19826
|
const vaultPath2 = getVaultPath();
|
|
19569
19827
|
switch (mode) {
|
|
19570
19828
|
case "status": {
|
|
@@ -19748,7 +20006,7 @@ function purgeOldMetrics(stateDb2, retentionDays = 90) {
|
|
|
19748
20006
|
}
|
|
19749
20007
|
|
|
19750
20008
|
// src/tools/read/metrics.ts
|
|
19751
|
-
function registerMetricsTools(server2, getIndex,
|
|
20009
|
+
function registerMetricsTools(server2, getIndex, getStateDb2) {
|
|
19752
20010
|
server2.registerTool(
|
|
19753
20011
|
"vault_growth",
|
|
19754
20012
|
{
|
|
@@ -19763,7 +20021,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
19763
20021
|
},
|
|
19764
20022
|
async ({ mode, metric, days_back, limit: eventLimit }) => {
|
|
19765
20023
|
const index = getIndex();
|
|
19766
|
-
const stateDb2 =
|
|
20024
|
+
const stateDb2 = getStateDb2();
|
|
19767
20025
|
const daysBack = days_back ?? 30;
|
|
19768
20026
|
let result;
|
|
19769
20027
|
switch (mode) {
|
|
@@ -19832,7 +20090,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
19832
20090
|
|
|
19833
20091
|
// src/tools/read/activity.ts
|
|
19834
20092
|
import { z as z30 } from "zod";
|
|
19835
|
-
function registerActivityTools(server2,
|
|
20093
|
+
function registerActivityTools(server2, getStateDb2, getSessionId2) {
|
|
19836
20094
|
server2.registerTool(
|
|
19837
20095
|
"vault_activity",
|
|
19838
20096
|
{
|
|
@@ -19846,7 +20104,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
19846
20104
|
}
|
|
19847
20105
|
},
|
|
19848
20106
|
async ({ mode, session_id, days_back, limit: resultLimit }) => {
|
|
19849
|
-
const stateDb2 =
|
|
20107
|
+
const stateDb2 = getStateDb2();
|
|
19850
20108
|
if (!stateDb2) {
|
|
19851
20109
|
return {
|
|
19852
20110
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -20181,7 +20439,7 @@ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, option
|
|
|
20181
20439
|
|
|
20182
20440
|
// src/tools/read/similarity.ts
|
|
20183
20441
|
init_embeddings();
|
|
20184
|
-
function registerSimilarityTools(server2, getIndex, getVaultPath,
|
|
20442
|
+
function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
20185
20443
|
server2.registerTool(
|
|
20186
20444
|
"find_similar",
|
|
20187
20445
|
{
|
|
@@ -20196,7 +20454,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
20196
20454
|
async ({ path: path33, limit, diversity }) => {
|
|
20197
20455
|
const index = getIndex();
|
|
20198
20456
|
const vaultPath2 = getVaultPath();
|
|
20199
|
-
const stateDb2 =
|
|
20457
|
+
const stateDb2 = getStateDb2();
|
|
20200
20458
|
if (!stateDb2) {
|
|
20201
20459
|
return {
|
|
20202
20460
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -20237,7 +20495,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
20237
20495
|
init_embeddings();
|
|
20238
20496
|
import { z as z32 } from "zod";
|
|
20239
20497
|
import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
|
|
20240
|
-
function registerSemanticTools(server2, getVaultPath,
|
|
20498
|
+
function registerSemanticTools(server2, getVaultPath, getStateDb2) {
|
|
20241
20499
|
server2.registerTool(
|
|
20242
20500
|
"init_semantic",
|
|
20243
20501
|
{
|
|
@@ -20250,7 +20508,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
20250
20508
|
}
|
|
20251
20509
|
},
|
|
20252
20510
|
async ({ force }) => {
|
|
20253
|
-
const stateDb2 =
|
|
20511
|
+
const stateDb2 = getStateDb2();
|
|
20254
20512
|
if (!stateDb2) {
|
|
20255
20513
|
return {
|
|
20256
20514
|
content: [{
|
|
@@ -20344,7 +20602,7 @@ import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs,
|
|
|
20344
20602
|
function normalizeName(name) {
|
|
20345
20603
|
return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
|
|
20346
20604
|
}
|
|
20347
|
-
function registerMergeTools2(server2,
|
|
20605
|
+
function registerMergeTools2(server2, getStateDb2) {
|
|
20348
20606
|
server2.tool(
|
|
20349
20607
|
"suggest_entity_merges",
|
|
20350
20608
|
"Find potential duplicate entities that could be merged based on name similarity",
|
|
@@ -20352,7 +20610,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
20352
20610
|
limit: z33.number().optional().default(50).describe("Maximum number of suggestions to return")
|
|
20353
20611
|
},
|
|
20354
20612
|
async ({ limit }) => {
|
|
20355
|
-
const stateDb2 =
|
|
20613
|
+
const stateDb2 = getStateDb2();
|
|
20356
20614
|
if (!stateDb2) {
|
|
20357
20615
|
return {
|
|
20358
20616
|
content: [{ type: "text", text: JSON.stringify({ suggestions: [], error: "StateDb not available" }) }]
|
|
@@ -20458,7 +20716,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
20458
20716
|
reason: z33.string().describe("Original suggestion reason")
|
|
20459
20717
|
},
|
|
20460
20718
|
async ({ source_path, target_path, source_name, target_name, reason }) => {
|
|
20461
|
-
const stateDb2 =
|
|
20719
|
+
const stateDb2 = getStateDb2();
|
|
20462
20720
|
if (!stateDb2) {
|
|
20463
20721
|
return {
|
|
20464
20722
|
content: [{ type: "text", text: JSON.stringify({ dismissed: false, error: "StateDb not available" }) }]
|
|
@@ -20993,7 +21251,7 @@ function handleTemporalSummary(index, stateDb2, startDate, endDate, focusEntitie
|
|
|
20993
21251
|
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
20994
21252
|
};
|
|
20995
21253
|
}
|
|
20996
|
-
function registerTemporalAnalysisTools(server2, getIndex, getVaultPath,
|
|
21254
|
+
function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
20997
21255
|
server2.registerTool(
|
|
20998
21256
|
"get_context_around_date",
|
|
20999
21257
|
{
|
|
@@ -21008,7 +21266,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
|
|
|
21008
21266
|
async ({ date, window_days, limit: requestedLimit }) => {
|
|
21009
21267
|
requireIndex();
|
|
21010
21268
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
21011
|
-
return handleGetContextAroundDate(getIndex(),
|
|
21269
|
+
return handleGetContextAroundDate(getIndex(), getStateDb2(), date, window_days ?? 3, limit);
|
|
21012
21270
|
}
|
|
21013
21271
|
);
|
|
21014
21272
|
server2.registerTool(
|
|
@@ -21030,7 +21288,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
|
|
|
21030
21288
|
const limit = Math.min(requestedLimit ?? 30, MAX_LIMIT);
|
|
21031
21289
|
return handlePredictStaleNotes(
|
|
21032
21290
|
getIndex(),
|
|
21033
|
-
|
|
21291
|
+
getStateDb2(),
|
|
21034
21292
|
days ?? 30,
|
|
21035
21293
|
min_importance ?? 0,
|
|
21036
21294
|
include_recommendations ?? true,
|
|
@@ -21055,7 +21313,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
|
|
|
21055
21313
|
requireIndex();
|
|
21056
21314
|
return handleTrackConceptEvolution(
|
|
21057
21315
|
getIndex(),
|
|
21058
|
-
|
|
21316
|
+
getStateDb2(),
|
|
21059
21317
|
entity,
|
|
21060
21318
|
days_back ?? 90,
|
|
21061
21319
|
include_cooccurrence ?? true
|
|
@@ -21079,7 +21337,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
|
|
|
21079
21337
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
21080
21338
|
return handleTemporalSummary(
|
|
21081
21339
|
getIndex(),
|
|
21082
|
-
|
|
21340
|
+
getStateDb2(),
|
|
21083
21341
|
start_date,
|
|
21084
21342
|
end_date,
|
|
21085
21343
|
focus_entities,
|
|
@@ -21094,6 +21352,7 @@ init_serverLog();
|
|
|
21094
21352
|
init_wikilinkFeedback();
|
|
21095
21353
|
init_recency();
|
|
21096
21354
|
init_cooccurrence();
|
|
21355
|
+
init_retrievalCooccurrence();
|
|
21097
21356
|
init_corrections();
|
|
21098
21357
|
init_edgeWeights();
|
|
21099
21358
|
import * as fs32 from "node:fs/promises";
|
|
@@ -21244,10 +21503,14 @@ function parseVaultConfig() {
|
|
|
21244
21503
|
for (const entry of envValue.split(",")) {
|
|
21245
21504
|
const trimmed = entry.trim();
|
|
21246
21505
|
const colonIdx = trimmed.indexOf(":");
|
|
21247
|
-
if (colonIdx <= 0)
|
|
21506
|
+
if (colonIdx <= 0) {
|
|
21507
|
+
console.error(`[flywheel] Warning: skipping malformed FLYWHEEL_VAULTS entry: "${trimmed}" (expected name:/path)`);
|
|
21508
|
+
continue;
|
|
21509
|
+
}
|
|
21248
21510
|
let name;
|
|
21249
21511
|
let vaultPath2;
|
|
21250
21512
|
if (colonIdx === 1 && trimmed.length > 2 && (trimmed[2] === "\\" || trimmed[2] === "/")) {
|
|
21513
|
+
console.error(`[flywheel] Warning: skipping ambiguous FLYWHEEL_VAULTS entry: "${trimmed}" (looks like a Windows path, not name:path)`);
|
|
21251
21514
|
continue;
|
|
21252
21515
|
}
|
|
21253
21516
|
name = trimmed.substring(0, colonIdx);
|
|
@@ -21271,6 +21534,11 @@ try {
|
|
|
21271
21534
|
} catch {
|
|
21272
21535
|
resolvedVaultPath = vaultPath.replace(/\\/g, "/");
|
|
21273
21536
|
}
|
|
21537
|
+
if (!existsSync3(resolvedVaultPath)) {
|
|
21538
|
+
console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
|
|
21539
|
+
console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
|
|
21540
|
+
process.exit(1);
|
|
21541
|
+
}
|
|
21274
21542
|
var vaultIndex;
|
|
21275
21543
|
var flywheelConfig = {};
|
|
21276
21544
|
var stateDb = null;
|
|
@@ -21574,7 +21842,7 @@ Use "note_intelligence" for per-note analysis (completeness, quality, suggestion
|
|
|
21574
21842
|
}
|
|
21575
21843
|
return parts.join("\n");
|
|
21576
21844
|
}
|
|
21577
|
-
function applyToolGating(targetServer, categories,
|
|
21845
|
+
function applyToolGating(targetServer, categories, getDb4, registry, getVaultPath) {
|
|
21578
21846
|
let _registered = 0;
|
|
21579
21847
|
let _skipped = 0;
|
|
21580
21848
|
function gate(name) {
|
|
@@ -21591,6 +21859,7 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21591
21859
|
const start = Date.now();
|
|
21592
21860
|
let success = true;
|
|
21593
21861
|
let notePaths;
|
|
21862
|
+
let result;
|
|
21594
21863
|
const params = args[0];
|
|
21595
21864
|
if (params && typeof params === "object") {
|
|
21596
21865
|
const paths = [];
|
|
@@ -21602,12 +21871,13 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21602
21871
|
if (paths.length > 0) notePaths = paths;
|
|
21603
21872
|
}
|
|
21604
21873
|
try {
|
|
21605
|
-
|
|
21874
|
+
result = await handler(...args);
|
|
21875
|
+
return result;
|
|
21606
21876
|
} catch (err) {
|
|
21607
21877
|
success = false;
|
|
21608
21878
|
throw err;
|
|
21609
21879
|
} finally {
|
|
21610
|
-
const db4 =
|
|
21880
|
+
const db4 = getDb4();
|
|
21611
21881
|
if (db4) {
|
|
21612
21882
|
try {
|
|
21613
21883
|
let sessionId;
|
|
@@ -21615,12 +21885,36 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21615
21885
|
sessionId = getSessionId();
|
|
21616
21886
|
} catch {
|
|
21617
21887
|
}
|
|
21888
|
+
let responseTokens;
|
|
21889
|
+
if (result?.content) {
|
|
21890
|
+
let totalChars = 0;
|
|
21891
|
+
for (const block of result.content) {
|
|
21892
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
21893
|
+
totalChars += block.text.length;
|
|
21894
|
+
}
|
|
21895
|
+
}
|
|
21896
|
+
if (totalChars > 0) responseTokens = Math.ceil(totalChars / 4);
|
|
21897
|
+
}
|
|
21898
|
+
let baselineTokens;
|
|
21899
|
+
if (notePaths && notePaths.length > 0 && getVaultPath) {
|
|
21900
|
+
const vp = getVaultPath();
|
|
21901
|
+
let totalBytes = 0;
|
|
21902
|
+
for (const p of notePaths) {
|
|
21903
|
+
try {
|
|
21904
|
+
totalBytes += statSync5(path32.join(vp, p)).size;
|
|
21905
|
+
} catch {
|
|
21906
|
+
}
|
|
21907
|
+
}
|
|
21908
|
+
if (totalBytes > 0) baselineTokens = Math.ceil(totalBytes / 4);
|
|
21909
|
+
}
|
|
21618
21910
|
recordToolInvocation(db4, {
|
|
21619
21911
|
tool_name: toolName,
|
|
21620
21912
|
session_id: sessionId,
|
|
21621
21913
|
note_paths: notePaths,
|
|
21622
21914
|
duration_ms: Date.now() - start,
|
|
21623
|
-
success
|
|
21915
|
+
success,
|
|
21916
|
+
response_tokens: responseTokens,
|
|
21917
|
+
baseline_tokens: baselineTokens
|
|
21624
21918
|
});
|
|
21625
21919
|
} catch {
|
|
21626
21920
|
}
|
|
@@ -21642,21 +21936,15 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21642
21936
|
}
|
|
21643
21937
|
const ctx = registry.getContext(vaultName);
|
|
21644
21938
|
activateVault(ctx);
|
|
21645
|
-
|
|
21646
|
-
vaultIndex = ctx.vaultIndex;
|
|
21647
|
-
flywheelConfig = ctx.flywheelConfig;
|
|
21648
|
-
return handler(...args);
|
|
21939
|
+
return runInVaultScope(buildVaultScope(ctx), () => handler(...args));
|
|
21649
21940
|
};
|
|
21650
21941
|
}
|
|
21651
21942
|
async function crossVaultSearch(reg, handler, args) {
|
|
21652
21943
|
const perVault = [];
|
|
21653
21944
|
for (const ctx of reg.getAllContexts()) {
|
|
21654
21945
|
activateVault(ctx);
|
|
21655
|
-
stateDb = ctx.stateDb;
|
|
21656
|
-
vaultIndex = ctx.vaultIndex;
|
|
21657
|
-
flywheelConfig = ctx.flywheelConfig;
|
|
21658
21946
|
try {
|
|
21659
|
-
const result = await handler(...args);
|
|
21947
|
+
const result = await runInVaultScope(buildVaultScope(ctx), () => handler(...args));
|
|
21660
21948
|
const text = result?.content?.[0]?.text;
|
|
21661
21949
|
if (text) {
|
|
21662
21950
|
perVault.push({ vault: ctx.name, data: JSON.parse(text) });
|
|
@@ -21738,39 +22026,43 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21738
22026
|
} };
|
|
21739
22027
|
}
|
|
21740
22028
|
function registerAllTools(targetServer) {
|
|
21741
|
-
|
|
22029
|
+
const gvp = () => getActiveScopeOrNull()?.vaultPath ?? vaultPath;
|
|
22030
|
+
const gvi = () => getActiveScopeOrNull()?.vaultIndex ?? vaultIndex;
|
|
22031
|
+
const gsd = () => getActiveScopeOrNull()?.stateDb ?? stateDb;
|
|
22032
|
+
const gcf = () => getActiveScopeOrNull()?.flywheelConfig ?? flywheelConfig;
|
|
22033
|
+
registerHealthTools(targetServer, gvi, gvp, gcf, gsd, getWatcherStatus);
|
|
21742
22034
|
registerSystemTools(
|
|
21743
22035
|
targetServer,
|
|
21744
|
-
|
|
22036
|
+
gvi,
|
|
21745
22037
|
(newIndex) => {
|
|
21746
22038
|
vaultIndex = newIndex;
|
|
21747
22039
|
},
|
|
21748
|
-
|
|
22040
|
+
gvp,
|
|
21749
22041
|
(newConfig) => {
|
|
21750
22042
|
flywheelConfig = newConfig;
|
|
21751
22043
|
setWikilinkConfig(newConfig);
|
|
21752
22044
|
},
|
|
21753
|
-
|
|
22045
|
+
gsd
|
|
21754
22046
|
);
|
|
21755
|
-
registerGraphTools(targetServer,
|
|
21756
|
-
registerWikilinkTools(targetServer,
|
|
21757
|
-
registerQueryTools(targetServer,
|
|
21758
|
-
registerPrimitiveTools(targetServer,
|
|
21759
|
-
registerGraphAnalysisTools(targetServer,
|
|
21760
|
-
registerSemanticAnalysisTools(targetServer,
|
|
21761
|
-
registerVaultSchemaTools(targetServer,
|
|
21762
|
-
registerNoteIntelligenceTools(targetServer,
|
|
21763
|
-
registerMigrationTools(targetServer,
|
|
21764
|
-
registerMutationTools(targetServer,
|
|
21765
|
-
registerTaskTools(targetServer,
|
|
21766
|
-
registerFrontmatterTools(targetServer,
|
|
21767
|
-
registerNoteTools(targetServer,
|
|
21768
|
-
registerMoveNoteTools(targetServer,
|
|
21769
|
-
registerMergeTools(targetServer,
|
|
21770
|
-
registerSystemTools2(targetServer,
|
|
21771
|
-
registerPolicyTools(targetServer,
|
|
21772
|
-
|
|
21773
|
-
|
|
22047
|
+
registerGraphTools(targetServer, gvi, gvp, gsd);
|
|
22048
|
+
registerWikilinkTools(targetServer, gvi, gvp, gsd);
|
|
22049
|
+
registerQueryTools(targetServer, gvi, gvp, gsd);
|
|
22050
|
+
registerPrimitiveTools(targetServer, gvi, gvp, gcf, gsd);
|
|
22051
|
+
registerGraphAnalysisTools(targetServer, gvi, gvp, gsd, gcf);
|
|
22052
|
+
registerSemanticAnalysisTools(targetServer, gvi, gvp);
|
|
22053
|
+
registerVaultSchemaTools(targetServer, gvi, gvp);
|
|
22054
|
+
registerNoteIntelligenceTools(targetServer, gvi, gvp, gcf);
|
|
22055
|
+
registerMigrationTools(targetServer, gvi, gvp);
|
|
22056
|
+
registerMutationTools(targetServer, gvp, gcf);
|
|
22057
|
+
registerTaskTools(targetServer, gvp);
|
|
22058
|
+
registerFrontmatterTools(targetServer, gvp);
|
|
22059
|
+
registerNoteTools(targetServer, gvp, gvi);
|
|
22060
|
+
registerMoveNoteTools(targetServer, gvp);
|
|
22061
|
+
registerMergeTools(targetServer, gvp);
|
|
22062
|
+
registerSystemTools2(targetServer, gvp);
|
|
22063
|
+
registerPolicyTools(targetServer, gvp, () => {
|
|
22064
|
+
const index = gvi();
|
|
22065
|
+
if (!index) return void 0;
|
|
21774
22066
|
return ({ query, folder, where, limit = 10 }) => {
|
|
21775
22067
|
let notes = Array.from(index.notes.values());
|
|
21776
22068
|
if (folder) {
|
|
@@ -21798,42 +22090,42 @@ function registerAllTools(targetServer) {
|
|
|
21798
22090
|
}));
|
|
21799
22091
|
};
|
|
21800
22092
|
});
|
|
21801
|
-
registerTagTools(targetServer,
|
|
21802
|
-
registerWikilinkFeedbackTools(targetServer,
|
|
21803
|
-
registerCorrectionTools(targetServer,
|
|
21804
|
-
registerInitTools(targetServer,
|
|
22093
|
+
registerTagTools(targetServer, gvi, gvp);
|
|
22094
|
+
registerWikilinkFeedbackTools(targetServer, gsd);
|
|
22095
|
+
registerCorrectionTools(targetServer, gsd);
|
|
22096
|
+
registerInitTools(targetServer, gvp, gsd);
|
|
21805
22097
|
registerConfigTools(
|
|
21806
22098
|
targetServer,
|
|
21807
|
-
|
|
22099
|
+
gcf,
|
|
21808
22100
|
(newConfig) => {
|
|
21809
22101
|
flywheelConfig = newConfig;
|
|
21810
22102
|
setWikilinkConfig(newConfig);
|
|
21811
22103
|
},
|
|
21812
|
-
|
|
22104
|
+
gsd
|
|
21813
22105
|
);
|
|
21814
|
-
registerMetricsTools(targetServer,
|
|
21815
|
-
registerActivityTools(targetServer,
|
|
22106
|
+
registerMetricsTools(targetServer, gvi, gsd);
|
|
22107
|
+
registerActivityTools(targetServer, gsd, () => {
|
|
21816
22108
|
try {
|
|
21817
22109
|
return getSessionId();
|
|
21818
22110
|
} catch {
|
|
21819
22111
|
return null;
|
|
21820
22112
|
}
|
|
21821
22113
|
});
|
|
21822
|
-
registerSimilarityTools(targetServer,
|
|
21823
|
-
registerSemanticTools(targetServer,
|
|
21824
|
-
registerMergeTools2(targetServer,
|
|
21825
|
-
registerTemporalAnalysisTools(targetServer,
|
|
21826
|
-
registerMemoryTools(targetServer,
|
|
21827
|
-
registerRecallTools(targetServer,
|
|
21828
|
-
registerBriefTools(targetServer,
|
|
21829
|
-
registerVaultResources(targetServer, () =>
|
|
22114
|
+
registerSimilarityTools(targetServer, gvi, gvp, gsd);
|
|
22115
|
+
registerSemanticTools(targetServer, gvp, gsd);
|
|
22116
|
+
registerMergeTools2(targetServer, gsd);
|
|
22117
|
+
registerTemporalAnalysisTools(targetServer, gvi, gvp, gsd);
|
|
22118
|
+
registerMemoryTools(targetServer, gsd);
|
|
22119
|
+
registerRecallTools(targetServer, gsd, gvp, () => gvi() ?? null);
|
|
22120
|
+
registerBriefTools(targetServer, gsd);
|
|
22121
|
+
registerVaultResources(targetServer, () => gvi() ?? null);
|
|
21830
22122
|
}
|
|
21831
22123
|
function createConfiguredServer() {
|
|
21832
22124
|
const s = new McpServer(
|
|
21833
22125
|
{ name: "flywheel-memory", version: pkg.version },
|
|
21834
22126
|
{ instructions: generateInstructions(enabledCategories, vaultRegistry) }
|
|
21835
22127
|
);
|
|
21836
|
-
applyToolGating(s, enabledCategories, () => stateDb, vaultRegistry);
|
|
22128
|
+
applyToolGating(s, enabledCategories, () => stateDb, vaultRegistry, () => vaultPath);
|
|
21837
22129
|
registerAllTools(s);
|
|
21838
22130
|
return s;
|
|
21839
22131
|
}
|
|
@@ -21841,7 +22133,7 @@ var server = new McpServer(
|
|
|
21841
22133
|
{ name: "flywheel-memory", version: pkg.version },
|
|
21842
22134
|
{ instructions: generateInstructions(enabledCategories, vaultRegistry) }
|
|
21843
22135
|
);
|
|
21844
|
-
var _gatingResult = applyToolGating(server, enabledCategories, () => stateDb, vaultRegistry);
|
|
22136
|
+
var _gatingResult = applyToolGating(server, enabledCategories, () => stateDb, vaultRegistry, () => vaultPath);
|
|
21845
22137
|
registerAllTools(server);
|
|
21846
22138
|
var categoryList = Array.from(enabledCategories).sort().join(", ");
|
|
21847
22139
|
serverLog("server", `Tool categories: ${categoryList}`);
|
|
@@ -21875,6 +22167,20 @@ async function initializeVault(name, vaultPathArg) {
|
|
|
21875
22167
|
}
|
|
21876
22168
|
return ctx;
|
|
21877
22169
|
}
|
|
22170
|
+
function buildVaultScope(ctx) {
|
|
22171
|
+
return {
|
|
22172
|
+
name: ctx.name,
|
|
22173
|
+
vaultPath: ctx.vaultPath,
|
|
22174
|
+
stateDb: ctx.stateDb,
|
|
22175
|
+
flywheelConfig: ctx.flywheelConfig,
|
|
22176
|
+
vaultIndex: ctx.vaultIndex,
|
|
22177
|
+
cooccurrenceIndex: ctx.cooccurrenceIndex,
|
|
22178
|
+
indexState: ctx.indexState,
|
|
22179
|
+
indexError: ctx.indexError,
|
|
22180
|
+
embeddingsBuilding: ctx.embeddingsBuilding,
|
|
22181
|
+
entityEmbeddingsMap: getEntityEmbeddingsMap()
|
|
22182
|
+
};
|
|
22183
|
+
}
|
|
21878
22184
|
function activateVault(ctx) {
|
|
21879
22185
|
globalThis.__flywheel_active_vault = ctx.name;
|
|
21880
22186
|
if (ctx.stateDb) {
|
|
@@ -21891,16 +22197,7 @@ function activateVault(ctx) {
|
|
|
21891
22197
|
setIndexState(ctx.indexState);
|
|
21892
22198
|
setIndexError(ctx.indexError);
|
|
21893
22199
|
setEmbeddingsBuilding(ctx.embeddingsBuilding);
|
|
21894
|
-
setActiveScope(
|
|
21895
|
-
name: ctx.name,
|
|
21896
|
-
vaultPath: ctx.vaultPath,
|
|
21897
|
-
stateDb: ctx.stateDb,
|
|
21898
|
-
flywheelConfig: ctx.flywheelConfig,
|
|
21899
|
-
cooccurrenceIndex: ctx.cooccurrenceIndex,
|
|
21900
|
-
indexState: ctx.indexState,
|
|
21901
|
-
indexError: ctx.indexError,
|
|
21902
|
-
embeddingsBuilding: ctx.embeddingsBuilding
|
|
21903
|
-
});
|
|
22200
|
+
setActiveScope(buildVaultScope(ctx));
|
|
21904
22201
|
}
|
|
21905
22202
|
function getActiveVaultContext() {
|
|
21906
22203
|
if (!vaultRegistry) return null;
|
|
@@ -21967,6 +22264,10 @@ async function main() {
|
|
|
21967
22264
|
const { createMcpExpressApp } = await import("@modelcontextprotocol/sdk/server/express.js");
|
|
21968
22265
|
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
21969
22266
|
const httpPort = parseInt(process.env.FLYWHEEL_HTTP_PORT ?? "3111", 10);
|
|
22267
|
+
if (!Number.isFinite(httpPort) || httpPort < 1 || httpPort > 65535) {
|
|
22268
|
+
console.error(`[flywheel] Fatal: invalid FLYWHEEL_HTTP_PORT: ${process.env.FLYWHEEL_HTTP_PORT} (must be 1-65535)`);
|
|
22269
|
+
process.exit(1);
|
|
22270
|
+
}
|
|
21970
22271
|
const httpHost = process.env.FLYWHEEL_HTTP_HOST ?? "127.0.0.1";
|
|
21971
22272
|
const app = createMcpExpressApp({ host: httpHost });
|
|
21972
22273
|
app.post("/mcp", async (req, res) => {
|
|
@@ -22139,6 +22440,7 @@ function runPeriodicMaintenance(db4) {
|
|
|
22139
22440
|
purgeOldSuggestionEvents(db4, 30);
|
|
22140
22441
|
purgeOldNoteLinkHistory(db4, 90);
|
|
22141
22442
|
purgeOldSnapshots(db4, 90);
|
|
22443
|
+
pruneStaleRetrievalCooccurrence(db4, 30);
|
|
22142
22444
|
lastPurgeAt = now;
|
|
22143
22445
|
serverLog("server", "Daily purge complete");
|
|
22144
22446
|
}
|
|
@@ -23012,6 +23314,21 @@ async function runPostIndexWork(index) {
|
|
|
23012
23314
|
tracker.end({ error: String(e) });
|
|
23013
23315
|
serverLog("watcher", `Tag scan: failed: ${e}`, "error");
|
|
23014
23316
|
}
|
|
23317
|
+
tracker.start("retrieval_cooccurrence", {});
|
|
23318
|
+
try {
|
|
23319
|
+
if (stateDb) {
|
|
23320
|
+
const inserted = mineRetrievalCooccurrence(stateDb);
|
|
23321
|
+
tracker.end({ pairs_inserted: inserted });
|
|
23322
|
+
if (inserted > 0) {
|
|
23323
|
+
serverLog("watcher", `Retrieval co-occurrence: ${inserted} new pairs`);
|
|
23324
|
+
}
|
|
23325
|
+
} else {
|
|
23326
|
+
tracker.end({ skipped: "no stateDb" });
|
|
23327
|
+
}
|
|
23328
|
+
} catch (e) {
|
|
23329
|
+
tracker.end({ error: String(e) });
|
|
23330
|
+
serverLog("watcher", `Retrieval co-occurrence: failed: ${e}`, "error");
|
|
23331
|
+
}
|
|
23015
23332
|
const duration = Date.now() - batchStart;
|
|
23016
23333
|
if (stateDb) {
|
|
23017
23334
|
recordIndexEvent(stateDb, {
|