@velvetmonkey/flywheel-memory 2.0.119 → 2.0.120
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 +529 -213
- 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,20 @@ 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
|
+
const stateDb2 = getStateDb2();
|
|
9912
|
+
if (stateDb2) {
|
|
9913
|
+
try {
|
|
9914
|
+
const result = stateDb2.db.pragma("quick_check");
|
|
9915
|
+
const ok = result.length === 1 && Object.values(result[0])[0] === "ok";
|
|
9916
|
+
if (!ok) {
|
|
9917
|
+
overall = "unhealthy";
|
|
9918
|
+
recommendations.push(`Database integrity check failed: ${Object.values(result[0])[0] ?? "unknown error"}`);
|
|
9919
|
+
}
|
|
9920
|
+
} catch (err) {
|
|
9921
|
+
overall = "unhealthy";
|
|
9922
|
+
recommendations.push(`Database integrity check error: ${err instanceof Error ? err.message : err}`);
|
|
9923
|
+
}
|
|
9924
|
+
}
|
|
9724
9925
|
const indexBuilt = indexState2 === "ready" && index !== void 0 && index.notes !== void 0;
|
|
9725
9926
|
const indexAge = indexBuilt && index.builtAt ? Math.floor((Date.now() - index.builtAt.getTime()) / 1e3) : -1;
|
|
9726
9927
|
const indexStale = indexBuilt && indexAge > STALE_THRESHOLD_SECONDS;
|
|
@@ -9769,7 +9970,6 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
9769
9970
|
const config = getConfig2();
|
|
9770
9971
|
const configInfo = Object.keys(config).length > 0 ? config : void 0;
|
|
9771
9972
|
let lastRebuild;
|
|
9772
|
-
const stateDb2 = getStateDb();
|
|
9773
9973
|
if (stateDb2) {
|
|
9774
9974
|
try {
|
|
9775
9975
|
const events = getRecentIndexEvents(stateDb2, 1);
|
|
@@ -10071,7 +10271,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
|
|
|
10071
10271
|
const checks = [];
|
|
10072
10272
|
const index = getIndex();
|
|
10073
10273
|
const vaultPath2 = getVaultPath();
|
|
10074
|
-
const stateDb2 =
|
|
10274
|
+
const stateDb2 = getStateDb2();
|
|
10075
10275
|
const watcherStatus = getWatcherStatus2();
|
|
10076
10276
|
checks.push({
|
|
10077
10277
|
name: "schema_version",
|
|
@@ -10517,6 +10717,7 @@ function enrichNoteResult(notePath, stateDb2, index) {
|
|
|
10517
10717
|
}
|
|
10518
10718
|
|
|
10519
10719
|
// src/tools/read/query.ts
|
|
10720
|
+
init_wikilinkFeedback();
|
|
10520
10721
|
function matchesFrontmatter(note, where) {
|
|
10521
10722
|
for (const [key, value] of Object.entries(where)) {
|
|
10522
10723
|
const noteValue = note.frontmatter[key];
|
|
@@ -10584,7 +10785,7 @@ function sortNotes(notes, sortBy, order) {
|
|
|
10584
10785
|
});
|
|
10585
10786
|
return sorted;
|
|
10586
10787
|
}
|
|
10587
|
-
function registerQueryTools(server2, getIndex, getVaultPath,
|
|
10788
|
+
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
10588
10789
|
server2.tool(
|
|
10589
10790
|
"search",
|
|
10590
10791
|
'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 +10819,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10618
10819
|
const index = getIndex();
|
|
10619
10820
|
const vaultPath2 = getVaultPath();
|
|
10620
10821
|
if (prefix && query) {
|
|
10621
|
-
const stateDb2 =
|
|
10822
|
+
const stateDb2 = getStateDb2();
|
|
10622
10823
|
if (!stateDb2) {
|
|
10623
10824
|
return { content: [{ type: "text", text: JSON.stringify({ results: [], count: 0, query, error: "StateDb not initialized" }, null, 2) }] };
|
|
10624
10825
|
}
|
|
@@ -10666,7 +10867,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10666
10867
|
matchingNotes = sortNotes(matchingNotes, sort_by ?? "modified", order ?? "desc");
|
|
10667
10868
|
const totalMatches = matchingNotes.length;
|
|
10668
10869
|
const limitedNotes = matchingNotes.slice(0, limit);
|
|
10669
|
-
const stateDb2 =
|
|
10870
|
+
const stateDb2 = getStateDb2();
|
|
10670
10871
|
const notes = limitedNotes.map(
|
|
10671
10872
|
(note, i) => (i < detailN ? enrichResult : enrichResultLight)({ path: note.path, title: note.title }, index, stateDb2)
|
|
10672
10873
|
);
|
|
@@ -10694,7 +10895,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10694
10895
|
}
|
|
10695
10896
|
const fts5Results = searchFTS5(vaultPath2, query, limit);
|
|
10696
10897
|
let entityResults = [];
|
|
10697
|
-
const stateDbEntity =
|
|
10898
|
+
const stateDbEntity = getStateDb2();
|
|
10698
10899
|
if (stateDbEntity) {
|
|
10699
10900
|
try {
|
|
10700
10901
|
entityResults = searchEntities(stateDbEntity, query, limit);
|
|
@@ -10703,7 +10904,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10703
10904
|
}
|
|
10704
10905
|
let edgeRanked = [];
|
|
10705
10906
|
if (context_note) {
|
|
10706
|
-
const ctxStateDb =
|
|
10907
|
+
const ctxStateDb = getStateDb2();
|
|
10707
10908
|
if (ctxStateDb) {
|
|
10708
10909
|
try {
|
|
10709
10910
|
const edgeRows = ctxStateDb.db.prepare(`
|
|
@@ -10768,7 +10969,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10768
10969
|
}));
|
|
10769
10970
|
scored.sort((a, b) => b.rrf_score - a.rrf_score);
|
|
10770
10971
|
const filtered = applyFolderFilter(scored);
|
|
10771
|
-
const stateDb2 =
|
|
10972
|
+
const stateDb2 = getStateDb2();
|
|
10772
10973
|
const results = filtered.slice(0, limit).map((item, i) => ({
|
|
10773
10974
|
...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
|
|
10774
10975
|
rrf_score: item.rrf_score,
|
|
@@ -10776,6 +10977,34 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10776
10977
|
in_semantic: item.in_semantic,
|
|
10777
10978
|
in_entity: item.in_entity
|
|
10778
10979
|
}));
|
|
10980
|
+
if (stateDb2 && results.length < limit) {
|
|
10981
|
+
const existingPaths = new Set(results.map((r) => r.path));
|
|
10982
|
+
const backfill = [];
|
|
10983
|
+
for (const r of results.slice(0, 3)) {
|
|
10984
|
+
const rPath = r.path;
|
|
10985
|
+
if (!rPath) continue;
|
|
10986
|
+
try {
|
|
10987
|
+
const outlinks = getStoredNoteLinks(stateDb2, rPath);
|
|
10988
|
+
for (const target of outlinks) {
|
|
10989
|
+
const entityRow = stateDb2.db.prepare(
|
|
10990
|
+
"SELECT path FROM entities WHERE name_lower = ?"
|
|
10991
|
+
).get(target);
|
|
10992
|
+
if (entityRow?.path && !existingPaths.has(entityRow.path)) {
|
|
10993
|
+
existingPaths.add(entityRow.path);
|
|
10994
|
+
backfill.push({
|
|
10995
|
+
...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2),
|
|
10996
|
+
rrf_score: 0,
|
|
10997
|
+
in_fts5: false,
|
|
10998
|
+
in_semantic: false,
|
|
10999
|
+
in_entity: false
|
|
11000
|
+
});
|
|
11001
|
+
}
|
|
11002
|
+
}
|
|
11003
|
+
} catch {
|
|
11004
|
+
}
|
|
11005
|
+
}
|
|
11006
|
+
results.push(...backfill.slice(0, limit - results.length));
|
|
11007
|
+
}
|
|
10779
11008
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
10780
11009
|
method: "hybrid",
|
|
10781
11010
|
query,
|
|
@@ -10795,12 +11024,36 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10795
11024
|
...entityRanked.map((r) => ({ path: r.path, title: r.name, snippet: void 0, in_entity: true }))
|
|
10796
11025
|
];
|
|
10797
11026
|
const filtered = applyFolderFilter(mergedItems);
|
|
10798
|
-
const stateDb2 =
|
|
11027
|
+
const stateDb2 = getStateDb2();
|
|
10799
11028
|
const sliced = filtered.slice(0, limit);
|
|
10800
11029
|
const results = sliced.map((item, i) => ({
|
|
10801
11030
|
...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
|
|
10802
11031
|
..."in_fts5" in item ? { in_fts5: true } : { in_entity: true }
|
|
10803
11032
|
}));
|
|
11033
|
+
if (stateDb2 && results.length < limit) {
|
|
11034
|
+
const existingPaths = new Set(results.map((r) => r.path));
|
|
11035
|
+
const backfill = [];
|
|
11036
|
+
for (const r of results.slice(0, 3)) {
|
|
11037
|
+
const rPath = r.path;
|
|
11038
|
+
if (!rPath) continue;
|
|
11039
|
+
try {
|
|
11040
|
+
const outlinks = getStoredNoteLinks(stateDb2, rPath);
|
|
11041
|
+
for (const target of outlinks) {
|
|
11042
|
+
const entityRow = stateDb2.db.prepare(
|
|
11043
|
+
"SELECT path FROM entities WHERE name_lower = ?"
|
|
11044
|
+
).get(target);
|
|
11045
|
+
if (entityRow?.path && !existingPaths.has(entityRow.path)) {
|
|
11046
|
+
existingPaths.add(entityRow.path);
|
|
11047
|
+
backfill.push({
|
|
11048
|
+
...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2)
|
|
11049
|
+
});
|
|
11050
|
+
}
|
|
11051
|
+
}
|
|
11052
|
+
} catch {
|
|
11053
|
+
}
|
|
11054
|
+
}
|
|
11055
|
+
results.push(...backfill.slice(0, limit - results.length));
|
|
11056
|
+
}
|
|
10804
11057
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
10805
11058
|
method: "fts5",
|
|
10806
11059
|
query,
|
|
@@ -10808,7 +11061,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
10808
11061
|
results
|
|
10809
11062
|
}, null, 2) }] };
|
|
10810
11063
|
}
|
|
10811
|
-
const stateDbFts =
|
|
11064
|
+
const stateDbFts = getStateDb2();
|
|
10812
11065
|
const fts5Filtered = applyFolderFilter(fts5Results);
|
|
10813
11066
|
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
11067
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
@@ -10896,7 +11149,7 @@ function suggestEntityAliases(stateDb2, folder) {
|
|
|
10896
11149
|
|
|
10897
11150
|
// src/tools/read/system.ts
|
|
10898
11151
|
init_edgeWeights();
|
|
10899
|
-
function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig,
|
|
11152
|
+
function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb2) {
|
|
10900
11153
|
const RefreshIndexOutputSchema = {
|
|
10901
11154
|
success: z5.boolean().describe("Whether the refresh succeeded"),
|
|
10902
11155
|
notes_count: z5.number().describe("Number of notes indexed"),
|
|
@@ -10922,7 +11175,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
10922
11175
|
const newIndex = await buildVaultIndex(vaultPath2);
|
|
10923
11176
|
setIndex(newIndex);
|
|
10924
11177
|
setIndexState("ready");
|
|
10925
|
-
const stateDb2 =
|
|
11178
|
+
const stateDb2 = getStateDb2?.();
|
|
10926
11179
|
if (stateDb2) {
|
|
10927
11180
|
try {
|
|
10928
11181
|
const entityIndex2 = await scanVaultEntities2(vaultPath2, {
|
|
@@ -11009,7 +11262,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
11009
11262
|
setIndexState("error");
|
|
11010
11263
|
setIndexError(err instanceof Error ? err : new Error(String(err)));
|
|
11011
11264
|
const duration = Date.now() - startTime;
|
|
11012
|
-
const stateDb2 =
|
|
11265
|
+
const stateDb2 = getStateDb2?.();
|
|
11013
11266
|
if (stateDb2) {
|
|
11014
11267
|
recordIndexEvent(stateDb2, {
|
|
11015
11268
|
trigger: "manual_refresh",
|
|
@@ -11267,7 +11520,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
11267
11520
|
category,
|
|
11268
11521
|
limit: perCategoryLimit
|
|
11269
11522
|
}) => {
|
|
11270
|
-
const stateDb2 =
|
|
11523
|
+
const stateDb2 = getStateDb2?.();
|
|
11271
11524
|
if (!stateDb2) {
|
|
11272
11525
|
return {
|
|
11273
11526
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -11322,7 +11575,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
11322
11575
|
folder,
|
|
11323
11576
|
limit: requestedLimit
|
|
11324
11577
|
}) => {
|
|
11325
|
-
const stateDb2 =
|
|
11578
|
+
const stateDb2 = getStateDb2?.();
|
|
11326
11579
|
if (!stateDb2) {
|
|
11327
11580
|
return {
|
|
11328
11581
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -11542,7 +11795,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
11542
11795
|
|
|
11543
11796
|
// src/tools/read/primitives.ts
|
|
11544
11797
|
import { getEntityByName as getEntityByName3 } from "@velvetmonkey/vault-core";
|
|
11545
|
-
function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}),
|
|
11798
|
+
function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb2 = () => null) {
|
|
11546
11799
|
server2.registerTool(
|
|
11547
11800
|
"get_note_structure",
|
|
11548
11801
|
{
|
|
@@ -11581,7 +11834,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
|
|
|
11581
11834
|
enriched.backlink_count = backlinks.length;
|
|
11582
11835
|
enriched.outlink_count = note.outlinks.length;
|
|
11583
11836
|
}
|
|
11584
|
-
const stateDb2 =
|
|
11837
|
+
const stateDb2 = getStateDb2();
|
|
11585
11838
|
if (stateDb2 && note) {
|
|
11586
11839
|
try {
|
|
11587
11840
|
const entity = getEntityByName3(stateDb2, note.title);
|
|
@@ -12640,7 +12893,7 @@ function getExcludedPaths(index, config) {
|
|
|
12640
12893
|
}
|
|
12641
12894
|
return excluded;
|
|
12642
12895
|
}
|
|
12643
|
-
function registerGraphAnalysisTools(server2, getIndex, getVaultPath,
|
|
12896
|
+
function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb2, getConfig2) {
|
|
12644
12897
|
server2.registerTool(
|
|
12645
12898
|
"graph_analysis",
|
|
12646
12899
|
{
|
|
@@ -12810,7 +13063,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
|
|
|
12810
13063
|
};
|
|
12811
13064
|
}
|
|
12812
13065
|
case "emerging_hubs": {
|
|
12813
|
-
const db4 =
|
|
13066
|
+
const db4 = getStateDb2?.();
|
|
12814
13067
|
if (!db4) {
|
|
12815
13068
|
return {
|
|
12816
13069
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -17558,7 +17811,7 @@ function registerTagTools(server2, getIndex, getVaultPath) {
|
|
|
17558
17811
|
// src/tools/write/wikilinkFeedback.ts
|
|
17559
17812
|
init_wikilinkFeedback();
|
|
17560
17813
|
import { z as z22 } from "zod";
|
|
17561
|
-
function registerWikilinkFeedbackTools(server2,
|
|
17814
|
+
function registerWikilinkFeedbackTools(server2, getStateDb2) {
|
|
17562
17815
|
server2.registerTool(
|
|
17563
17816
|
"wikilink_feedback",
|
|
17564
17817
|
{
|
|
@@ -17579,7 +17832,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
17579
17832
|
}
|
|
17580
17833
|
},
|
|
17581
17834
|
async ({ mode, entity, note_path, context, correct, limit, days_back, granularity, timestamp_before, timestamp_after, skip_status_update }) => {
|
|
17582
|
-
const stateDb2 =
|
|
17835
|
+
const stateDb2 = getStateDb2();
|
|
17583
17836
|
if (!stateDb2) {
|
|
17584
17837
|
return {
|
|
17585
17838
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available \u2014 database not initialized yet" }) }],
|
|
@@ -17741,7 +17994,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
17741
17994
|
// src/tools/write/corrections.ts
|
|
17742
17995
|
init_corrections();
|
|
17743
17996
|
import { z as z23 } from "zod";
|
|
17744
|
-
function registerCorrectionTools(server2,
|
|
17997
|
+
function registerCorrectionTools(server2, getStateDb2) {
|
|
17745
17998
|
server2.tool(
|
|
17746
17999
|
"vault_record_correction",
|
|
17747
18000
|
'Record a persistent correction (e.g., "that link is wrong", "undo that"). Survives across sessions.',
|
|
@@ -17752,7 +18005,7 @@ function registerCorrectionTools(server2, getStateDb) {
|
|
|
17752
18005
|
note_path: z23.string().optional().describe("Note path (if correction is about a specific note)")
|
|
17753
18006
|
},
|
|
17754
18007
|
async ({ correction_type, description, entity, note_path }) => {
|
|
17755
|
-
const stateDb2 =
|
|
18008
|
+
const stateDb2 = getStateDb2();
|
|
17756
18009
|
if (!stateDb2) {
|
|
17757
18010
|
return {
|
|
17758
18011
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -17780,7 +18033,7 @@ function registerCorrectionTools(server2, getStateDb) {
|
|
|
17780
18033
|
limit: z23.number().min(1).max(200).default(50).describe("Max entries to return")
|
|
17781
18034
|
},
|
|
17782
18035
|
async ({ status, entity, limit }) => {
|
|
17783
|
-
const stateDb2 =
|
|
18036
|
+
const stateDb2 = getStateDb2();
|
|
17784
18037
|
if (!stateDb2) {
|
|
17785
18038
|
return {
|
|
17786
18039
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -17807,7 +18060,7 @@ function registerCorrectionTools(server2, getStateDb) {
|
|
|
17807
18060
|
status: z23.enum(["applied", "dismissed"]).describe("New status")
|
|
17808
18061
|
},
|
|
17809
18062
|
async ({ correction_id, status }) => {
|
|
17810
|
-
const stateDb2 =
|
|
18063
|
+
const stateDb2 = getStateDb2();
|
|
17811
18064
|
if (!stateDb2) {
|
|
17812
18065
|
return {
|
|
17813
18066
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -18155,7 +18408,7 @@ function findContradictions2(stateDb2, entity) {
|
|
|
18155
18408
|
}
|
|
18156
18409
|
|
|
18157
18410
|
// src/tools/write/memory.ts
|
|
18158
|
-
function registerMemoryTools(server2,
|
|
18411
|
+
function registerMemoryTools(server2, getStateDb2) {
|
|
18159
18412
|
server2.tool(
|
|
18160
18413
|
"memory",
|
|
18161
18414
|
"Store, retrieve, search, and manage agent working memory. Actions: store, get, search, list, forget, summarize_session.",
|
|
@@ -18180,7 +18433,7 @@ function registerMemoryTools(server2, getStateDb) {
|
|
|
18180
18433
|
tool_count: z24.number().optional().describe("Number of tool calls in session")
|
|
18181
18434
|
},
|
|
18182
18435
|
async (args) => {
|
|
18183
|
-
const stateDb2 =
|
|
18436
|
+
const stateDb2 = getStateDb2();
|
|
18184
18437
|
if (!stateDb2) {
|
|
18185
18438
|
return {
|
|
18186
18439
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -18771,7 +19024,7 @@ async function performRecall(stateDb2, query, options = {}) {
|
|
|
18771
19024
|
}
|
|
18772
19025
|
return selected;
|
|
18773
19026
|
}
|
|
18774
|
-
function registerRecallTools(server2,
|
|
19027
|
+
function registerRecallTools(server2, getStateDb2, getVaultPath, getIndex) {
|
|
18775
19028
|
server2.tool(
|
|
18776
19029
|
"recall",
|
|
18777
19030
|
"Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
|
|
@@ -18784,7 +19037,7 @@ function registerRecallTools(server2, getStateDb, getVaultPath, getIndex) {
|
|
|
18784
19037
|
diversity: z25.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
|
|
18785
19038
|
},
|
|
18786
19039
|
async (args) => {
|
|
18787
|
-
const stateDb2 =
|
|
19040
|
+
const stateDb2 = getStateDb2();
|
|
18788
19041
|
if (!stateDb2) {
|
|
18789
19042
|
return {
|
|
18790
19043
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -18840,15 +19093,17 @@ import { z as z26 } from "zod";
|
|
|
18840
19093
|
// src/core/shared/toolTracking.ts
|
|
18841
19094
|
function recordToolInvocation(stateDb2, event) {
|
|
18842
19095
|
stateDb2.db.prepare(
|
|
18843
|
-
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
|
|
18844
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
19096
|
+
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success, response_tokens, baseline_tokens)
|
|
19097
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
18845
19098
|
).run(
|
|
18846
19099
|
Date.now(),
|
|
18847
19100
|
event.tool_name,
|
|
18848
19101
|
event.session_id ?? null,
|
|
18849
19102
|
event.note_paths ? JSON.stringify(event.note_paths) : null,
|
|
18850
19103
|
event.duration_ms ?? null,
|
|
18851
|
-
event.success !== false ? 1 : 0
|
|
19104
|
+
event.success !== false ? 1 : 0,
|
|
19105
|
+
event.response_tokens ?? null,
|
|
19106
|
+
event.baseline_tokens ?? null
|
|
18852
19107
|
);
|
|
18853
19108
|
}
|
|
18854
19109
|
function rowToInvocation(row) {
|
|
@@ -18859,7 +19114,9 @@ function rowToInvocation(row) {
|
|
|
18859
19114
|
session_id: row.session_id,
|
|
18860
19115
|
note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
|
|
18861
19116
|
duration_ms: row.duration_ms,
|
|
18862
|
-
success: row.success === 1
|
|
19117
|
+
success: row.success === 1,
|
|
19118
|
+
response_tokens: row.response_tokens,
|
|
19119
|
+
baseline_tokens: row.baseline_tokens
|
|
18863
19120
|
};
|
|
18864
19121
|
}
|
|
18865
19122
|
function getToolUsageSummary(stateDb2, daysBack = 30) {
|
|
@@ -19119,7 +19376,7 @@ function buildVaultPulseSection(stateDb2) {
|
|
|
19119
19376
|
estimated_tokens: estimateTokens2(content)
|
|
19120
19377
|
};
|
|
19121
19378
|
}
|
|
19122
|
-
function registerBriefTools(server2,
|
|
19379
|
+
function registerBriefTools(server2, getStateDb2) {
|
|
19123
19380
|
server2.tool(
|
|
19124
19381
|
"brief",
|
|
19125
19382
|
"Get a startup context briefing: recent sessions, active entities, memories, pending corrections, and vault stats. Call at conversation start.",
|
|
@@ -19128,7 +19385,7 @@ function registerBriefTools(server2, getStateDb) {
|
|
|
19128
19385
|
focus: z26.string().optional().describe("Focus entity or topic (filters content)")
|
|
19129
19386
|
},
|
|
19130
19387
|
async (args) => {
|
|
19131
|
-
const stateDb2 =
|
|
19388
|
+
const stateDb2 = getStateDb2();
|
|
19132
19389
|
if (!stateDb2) {
|
|
19133
19390
|
return {
|
|
19134
19391
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
@@ -19189,7 +19446,7 @@ var VALID_CONFIG_KEYS = {
|
|
|
19189
19446
|
implicit_patterns: z27.array(z27.string()),
|
|
19190
19447
|
adaptive_strictness: z27.boolean()
|
|
19191
19448
|
};
|
|
19192
|
-
function registerConfigTools(server2, getConfig2, setConfig,
|
|
19449
|
+
function registerConfigTools(server2, getConfig2, setConfig, getStateDb2) {
|
|
19193
19450
|
server2.registerTool(
|
|
19194
19451
|
"flywheel_config",
|
|
19195
19452
|
{
|
|
@@ -19231,7 +19488,7 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb) {
|
|
|
19231
19488
|
}) }]
|
|
19232
19489
|
};
|
|
19233
19490
|
}
|
|
19234
|
-
const stateDb2 =
|
|
19491
|
+
const stateDb2 = getStateDb2();
|
|
19235
19492
|
if (!stateDb2) {
|
|
19236
19493
|
return {
|
|
19237
19494
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -19553,7 +19810,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
|
|
|
19553
19810
|
total_enriched: currentTotal
|
|
19554
19811
|
};
|
|
19555
19812
|
}
|
|
19556
|
-
function registerInitTools(server2, getVaultPath,
|
|
19813
|
+
function registerInitTools(server2, getVaultPath, getStateDb2) {
|
|
19557
19814
|
server2.tool(
|
|
19558
19815
|
"vault_init",
|
|
19559
19816
|
`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 +19821,7 @@ function registerInitTools(server2, getVaultPath, getStateDb) {
|
|
|
19564
19821
|
offset: z28.number().default(0).describe("For enrich mode: skip this many eligible notes (for pagination)")
|
|
19565
19822
|
},
|
|
19566
19823
|
async ({ mode, dry_run, batch_size, offset }) => {
|
|
19567
|
-
const stateDb2 =
|
|
19824
|
+
const stateDb2 = getStateDb2();
|
|
19568
19825
|
const vaultPath2 = getVaultPath();
|
|
19569
19826
|
switch (mode) {
|
|
19570
19827
|
case "status": {
|
|
@@ -19748,7 +20005,7 @@ function purgeOldMetrics(stateDb2, retentionDays = 90) {
|
|
|
19748
20005
|
}
|
|
19749
20006
|
|
|
19750
20007
|
// src/tools/read/metrics.ts
|
|
19751
|
-
function registerMetricsTools(server2, getIndex,
|
|
20008
|
+
function registerMetricsTools(server2, getIndex, getStateDb2) {
|
|
19752
20009
|
server2.registerTool(
|
|
19753
20010
|
"vault_growth",
|
|
19754
20011
|
{
|
|
@@ -19763,7 +20020,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
19763
20020
|
},
|
|
19764
20021
|
async ({ mode, metric, days_back, limit: eventLimit }) => {
|
|
19765
20022
|
const index = getIndex();
|
|
19766
|
-
const stateDb2 =
|
|
20023
|
+
const stateDb2 = getStateDb2();
|
|
19767
20024
|
const daysBack = days_back ?? 30;
|
|
19768
20025
|
let result;
|
|
19769
20026
|
switch (mode) {
|
|
@@ -19832,7 +20089,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
19832
20089
|
|
|
19833
20090
|
// src/tools/read/activity.ts
|
|
19834
20091
|
import { z as z30 } from "zod";
|
|
19835
|
-
function registerActivityTools(server2,
|
|
20092
|
+
function registerActivityTools(server2, getStateDb2, getSessionId2) {
|
|
19836
20093
|
server2.registerTool(
|
|
19837
20094
|
"vault_activity",
|
|
19838
20095
|
{
|
|
@@ -19846,7 +20103,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
19846
20103
|
}
|
|
19847
20104
|
},
|
|
19848
20105
|
async ({ mode, session_id, days_back, limit: resultLimit }) => {
|
|
19849
|
-
const stateDb2 =
|
|
20106
|
+
const stateDb2 = getStateDb2();
|
|
19850
20107
|
if (!stateDb2) {
|
|
19851
20108
|
return {
|
|
19852
20109
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -20181,7 +20438,7 @@ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, option
|
|
|
20181
20438
|
|
|
20182
20439
|
// src/tools/read/similarity.ts
|
|
20183
20440
|
init_embeddings();
|
|
20184
|
-
function registerSimilarityTools(server2, getIndex, getVaultPath,
|
|
20441
|
+
function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
20185
20442
|
server2.registerTool(
|
|
20186
20443
|
"find_similar",
|
|
20187
20444
|
{
|
|
@@ -20196,7 +20453,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
20196
20453
|
async ({ path: path33, limit, diversity }) => {
|
|
20197
20454
|
const index = getIndex();
|
|
20198
20455
|
const vaultPath2 = getVaultPath();
|
|
20199
|
-
const stateDb2 =
|
|
20456
|
+
const stateDb2 = getStateDb2();
|
|
20200
20457
|
if (!stateDb2) {
|
|
20201
20458
|
return {
|
|
20202
20459
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
@@ -20237,7 +20494,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
20237
20494
|
init_embeddings();
|
|
20238
20495
|
import { z as z32 } from "zod";
|
|
20239
20496
|
import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
|
|
20240
|
-
function registerSemanticTools(server2, getVaultPath,
|
|
20497
|
+
function registerSemanticTools(server2, getVaultPath, getStateDb2) {
|
|
20241
20498
|
server2.registerTool(
|
|
20242
20499
|
"init_semantic",
|
|
20243
20500
|
{
|
|
@@ -20250,7 +20507,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
20250
20507
|
}
|
|
20251
20508
|
},
|
|
20252
20509
|
async ({ force }) => {
|
|
20253
|
-
const stateDb2 =
|
|
20510
|
+
const stateDb2 = getStateDb2();
|
|
20254
20511
|
if (!stateDb2) {
|
|
20255
20512
|
return {
|
|
20256
20513
|
content: [{
|
|
@@ -20344,7 +20601,7 @@ import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs,
|
|
|
20344
20601
|
function normalizeName(name) {
|
|
20345
20602
|
return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
|
|
20346
20603
|
}
|
|
20347
|
-
function registerMergeTools2(server2,
|
|
20604
|
+
function registerMergeTools2(server2, getStateDb2) {
|
|
20348
20605
|
server2.tool(
|
|
20349
20606
|
"suggest_entity_merges",
|
|
20350
20607
|
"Find potential duplicate entities that could be merged based on name similarity",
|
|
@@ -20352,7 +20609,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
20352
20609
|
limit: z33.number().optional().default(50).describe("Maximum number of suggestions to return")
|
|
20353
20610
|
},
|
|
20354
20611
|
async ({ limit }) => {
|
|
20355
|
-
const stateDb2 =
|
|
20612
|
+
const stateDb2 = getStateDb2();
|
|
20356
20613
|
if (!stateDb2) {
|
|
20357
20614
|
return {
|
|
20358
20615
|
content: [{ type: "text", text: JSON.stringify({ suggestions: [], error: "StateDb not available" }) }]
|
|
@@ -20458,7 +20715,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
20458
20715
|
reason: z33.string().describe("Original suggestion reason")
|
|
20459
20716
|
},
|
|
20460
20717
|
async ({ source_path, target_path, source_name, target_name, reason }) => {
|
|
20461
|
-
const stateDb2 =
|
|
20718
|
+
const stateDb2 = getStateDb2();
|
|
20462
20719
|
if (!stateDb2) {
|
|
20463
20720
|
return {
|
|
20464
20721
|
content: [{ type: "text", text: JSON.stringify({ dismissed: false, error: "StateDb not available" }) }]
|
|
@@ -20993,7 +21250,7 @@ function handleTemporalSummary(index, stateDb2, startDate, endDate, focusEntitie
|
|
|
20993
21250
|
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
20994
21251
|
};
|
|
20995
21252
|
}
|
|
20996
|
-
function registerTemporalAnalysisTools(server2, getIndex, getVaultPath,
|
|
21253
|
+
function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getStateDb2) {
|
|
20997
21254
|
server2.registerTool(
|
|
20998
21255
|
"get_context_around_date",
|
|
20999
21256
|
{
|
|
@@ -21008,7 +21265,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
|
|
|
21008
21265
|
async ({ date, window_days, limit: requestedLimit }) => {
|
|
21009
21266
|
requireIndex();
|
|
21010
21267
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
21011
|
-
return handleGetContextAroundDate(getIndex(),
|
|
21268
|
+
return handleGetContextAroundDate(getIndex(), getStateDb2(), date, window_days ?? 3, limit);
|
|
21012
21269
|
}
|
|
21013
21270
|
);
|
|
21014
21271
|
server2.registerTool(
|
|
@@ -21030,7 +21287,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
|
|
|
21030
21287
|
const limit = Math.min(requestedLimit ?? 30, MAX_LIMIT);
|
|
21031
21288
|
return handlePredictStaleNotes(
|
|
21032
21289
|
getIndex(),
|
|
21033
|
-
|
|
21290
|
+
getStateDb2(),
|
|
21034
21291
|
days ?? 30,
|
|
21035
21292
|
min_importance ?? 0,
|
|
21036
21293
|
include_recommendations ?? true,
|
|
@@ -21055,7 +21312,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
|
|
|
21055
21312
|
requireIndex();
|
|
21056
21313
|
return handleTrackConceptEvolution(
|
|
21057
21314
|
getIndex(),
|
|
21058
|
-
|
|
21315
|
+
getStateDb2(),
|
|
21059
21316
|
entity,
|
|
21060
21317
|
days_back ?? 90,
|
|
21061
21318
|
include_cooccurrence ?? true
|
|
@@ -21079,7 +21336,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
|
|
|
21079
21336
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
21080
21337
|
return handleTemporalSummary(
|
|
21081
21338
|
getIndex(),
|
|
21082
|
-
|
|
21339
|
+
getStateDb2(),
|
|
21083
21340
|
start_date,
|
|
21084
21341
|
end_date,
|
|
21085
21342
|
focus_entities,
|
|
@@ -21094,6 +21351,7 @@ init_serverLog();
|
|
|
21094
21351
|
init_wikilinkFeedback();
|
|
21095
21352
|
init_recency();
|
|
21096
21353
|
init_cooccurrence();
|
|
21354
|
+
init_retrievalCooccurrence();
|
|
21097
21355
|
init_corrections();
|
|
21098
21356
|
init_edgeWeights();
|
|
21099
21357
|
import * as fs32 from "node:fs/promises";
|
|
@@ -21244,10 +21502,14 @@ function parseVaultConfig() {
|
|
|
21244
21502
|
for (const entry of envValue.split(",")) {
|
|
21245
21503
|
const trimmed = entry.trim();
|
|
21246
21504
|
const colonIdx = trimmed.indexOf(":");
|
|
21247
|
-
if (colonIdx <= 0)
|
|
21505
|
+
if (colonIdx <= 0) {
|
|
21506
|
+
console.error(`[flywheel] Warning: skipping malformed FLYWHEEL_VAULTS entry: "${trimmed}" (expected name:/path)`);
|
|
21507
|
+
continue;
|
|
21508
|
+
}
|
|
21248
21509
|
let name;
|
|
21249
21510
|
let vaultPath2;
|
|
21250
21511
|
if (colonIdx === 1 && trimmed.length > 2 && (trimmed[2] === "\\" || trimmed[2] === "/")) {
|
|
21512
|
+
console.error(`[flywheel] Warning: skipping ambiguous FLYWHEEL_VAULTS entry: "${trimmed}" (looks like a Windows path, not name:path)`);
|
|
21251
21513
|
continue;
|
|
21252
21514
|
}
|
|
21253
21515
|
name = trimmed.substring(0, colonIdx);
|
|
@@ -21271,6 +21533,11 @@ try {
|
|
|
21271
21533
|
} catch {
|
|
21272
21534
|
resolvedVaultPath = vaultPath.replace(/\\/g, "/");
|
|
21273
21535
|
}
|
|
21536
|
+
if (!existsSync3(resolvedVaultPath)) {
|
|
21537
|
+
console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
|
|
21538
|
+
console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
|
|
21539
|
+
process.exit(1);
|
|
21540
|
+
}
|
|
21274
21541
|
var vaultIndex;
|
|
21275
21542
|
var flywheelConfig = {};
|
|
21276
21543
|
var stateDb = null;
|
|
@@ -21574,7 +21841,7 @@ Use "note_intelligence" for per-note analysis (completeness, quality, suggestion
|
|
|
21574
21841
|
}
|
|
21575
21842
|
return parts.join("\n");
|
|
21576
21843
|
}
|
|
21577
|
-
function applyToolGating(targetServer, categories,
|
|
21844
|
+
function applyToolGating(targetServer, categories, getDb4, registry, getVaultPath) {
|
|
21578
21845
|
let _registered = 0;
|
|
21579
21846
|
let _skipped = 0;
|
|
21580
21847
|
function gate(name) {
|
|
@@ -21591,6 +21858,7 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21591
21858
|
const start = Date.now();
|
|
21592
21859
|
let success = true;
|
|
21593
21860
|
let notePaths;
|
|
21861
|
+
let result;
|
|
21594
21862
|
const params = args[0];
|
|
21595
21863
|
if (params && typeof params === "object") {
|
|
21596
21864
|
const paths = [];
|
|
@@ -21602,12 +21870,13 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21602
21870
|
if (paths.length > 0) notePaths = paths;
|
|
21603
21871
|
}
|
|
21604
21872
|
try {
|
|
21605
|
-
|
|
21873
|
+
result = await handler(...args);
|
|
21874
|
+
return result;
|
|
21606
21875
|
} catch (err) {
|
|
21607
21876
|
success = false;
|
|
21608
21877
|
throw err;
|
|
21609
21878
|
} finally {
|
|
21610
|
-
const db4 =
|
|
21879
|
+
const db4 = getDb4();
|
|
21611
21880
|
if (db4) {
|
|
21612
21881
|
try {
|
|
21613
21882
|
let sessionId;
|
|
@@ -21615,12 +21884,36 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21615
21884
|
sessionId = getSessionId();
|
|
21616
21885
|
} catch {
|
|
21617
21886
|
}
|
|
21887
|
+
let responseTokens;
|
|
21888
|
+
if (result?.content) {
|
|
21889
|
+
let totalChars = 0;
|
|
21890
|
+
for (const block of result.content) {
|
|
21891
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
21892
|
+
totalChars += block.text.length;
|
|
21893
|
+
}
|
|
21894
|
+
}
|
|
21895
|
+
if (totalChars > 0) responseTokens = Math.ceil(totalChars / 4);
|
|
21896
|
+
}
|
|
21897
|
+
let baselineTokens;
|
|
21898
|
+
if (notePaths && notePaths.length > 0 && getVaultPath) {
|
|
21899
|
+
const vp = getVaultPath();
|
|
21900
|
+
let totalBytes = 0;
|
|
21901
|
+
for (const p of notePaths) {
|
|
21902
|
+
try {
|
|
21903
|
+
totalBytes += statSync5(path32.join(vp, p)).size;
|
|
21904
|
+
} catch {
|
|
21905
|
+
}
|
|
21906
|
+
}
|
|
21907
|
+
if (totalBytes > 0) baselineTokens = Math.ceil(totalBytes / 4);
|
|
21908
|
+
}
|
|
21618
21909
|
recordToolInvocation(db4, {
|
|
21619
21910
|
tool_name: toolName,
|
|
21620
21911
|
session_id: sessionId,
|
|
21621
21912
|
note_paths: notePaths,
|
|
21622
21913
|
duration_ms: Date.now() - start,
|
|
21623
|
-
success
|
|
21914
|
+
success,
|
|
21915
|
+
response_tokens: responseTokens,
|
|
21916
|
+
baseline_tokens: baselineTokens
|
|
21624
21917
|
});
|
|
21625
21918
|
} catch {
|
|
21626
21919
|
}
|
|
@@ -21642,21 +21935,15 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21642
21935
|
}
|
|
21643
21936
|
const ctx = registry.getContext(vaultName);
|
|
21644
21937
|
activateVault(ctx);
|
|
21645
|
-
|
|
21646
|
-
vaultIndex = ctx.vaultIndex;
|
|
21647
|
-
flywheelConfig = ctx.flywheelConfig;
|
|
21648
|
-
return handler(...args);
|
|
21938
|
+
return runInVaultScope(buildVaultScope(ctx), () => handler(...args));
|
|
21649
21939
|
};
|
|
21650
21940
|
}
|
|
21651
21941
|
async function crossVaultSearch(reg, handler, args) {
|
|
21652
21942
|
const perVault = [];
|
|
21653
21943
|
for (const ctx of reg.getAllContexts()) {
|
|
21654
21944
|
activateVault(ctx);
|
|
21655
|
-
stateDb = ctx.stateDb;
|
|
21656
|
-
vaultIndex = ctx.vaultIndex;
|
|
21657
|
-
flywheelConfig = ctx.flywheelConfig;
|
|
21658
21945
|
try {
|
|
21659
|
-
const result = await handler(...args);
|
|
21946
|
+
const result = await runInVaultScope(buildVaultScope(ctx), () => handler(...args));
|
|
21660
21947
|
const text = result?.content?.[0]?.text;
|
|
21661
21948
|
if (text) {
|
|
21662
21949
|
perVault.push({ vault: ctx.name, data: JSON.parse(text) });
|
|
@@ -21738,39 +22025,43 @@ function applyToolGating(targetServer, categories, getDb, registry) {
|
|
|
21738
22025
|
} };
|
|
21739
22026
|
}
|
|
21740
22027
|
function registerAllTools(targetServer) {
|
|
21741
|
-
|
|
22028
|
+
const gvp = () => getActiveScopeOrNull()?.vaultPath ?? vaultPath;
|
|
22029
|
+
const gvi = () => getActiveScopeOrNull()?.vaultIndex ?? vaultIndex;
|
|
22030
|
+
const gsd = () => getActiveScopeOrNull()?.stateDb ?? stateDb;
|
|
22031
|
+
const gcf = () => getActiveScopeOrNull()?.flywheelConfig ?? flywheelConfig;
|
|
22032
|
+
registerHealthTools(targetServer, gvi, gvp, gcf, gsd, getWatcherStatus);
|
|
21742
22033
|
registerSystemTools(
|
|
21743
22034
|
targetServer,
|
|
21744
|
-
|
|
22035
|
+
gvi,
|
|
21745
22036
|
(newIndex) => {
|
|
21746
22037
|
vaultIndex = newIndex;
|
|
21747
22038
|
},
|
|
21748
|
-
|
|
22039
|
+
gvp,
|
|
21749
22040
|
(newConfig) => {
|
|
21750
22041
|
flywheelConfig = newConfig;
|
|
21751
22042
|
setWikilinkConfig(newConfig);
|
|
21752
22043
|
},
|
|
21753
|
-
|
|
22044
|
+
gsd
|
|
21754
22045
|
);
|
|
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
|
-
|
|
22046
|
+
registerGraphTools(targetServer, gvi, gvp, gsd);
|
|
22047
|
+
registerWikilinkTools(targetServer, gvi, gvp, gsd);
|
|
22048
|
+
registerQueryTools(targetServer, gvi, gvp, gsd);
|
|
22049
|
+
registerPrimitiveTools(targetServer, gvi, gvp, gcf, gsd);
|
|
22050
|
+
registerGraphAnalysisTools(targetServer, gvi, gvp, gsd, gcf);
|
|
22051
|
+
registerSemanticAnalysisTools(targetServer, gvi, gvp);
|
|
22052
|
+
registerVaultSchemaTools(targetServer, gvi, gvp);
|
|
22053
|
+
registerNoteIntelligenceTools(targetServer, gvi, gvp, gcf);
|
|
22054
|
+
registerMigrationTools(targetServer, gvi, gvp);
|
|
22055
|
+
registerMutationTools(targetServer, gvp, gcf);
|
|
22056
|
+
registerTaskTools(targetServer, gvp);
|
|
22057
|
+
registerFrontmatterTools(targetServer, gvp);
|
|
22058
|
+
registerNoteTools(targetServer, gvp, gvi);
|
|
22059
|
+
registerMoveNoteTools(targetServer, gvp);
|
|
22060
|
+
registerMergeTools(targetServer, gvp);
|
|
22061
|
+
registerSystemTools2(targetServer, gvp);
|
|
22062
|
+
registerPolicyTools(targetServer, gvp, () => {
|
|
22063
|
+
const index = gvi();
|
|
22064
|
+
if (!index) return void 0;
|
|
21774
22065
|
return ({ query, folder, where, limit = 10 }) => {
|
|
21775
22066
|
let notes = Array.from(index.notes.values());
|
|
21776
22067
|
if (folder) {
|
|
@@ -21798,42 +22089,42 @@ function registerAllTools(targetServer) {
|
|
|
21798
22089
|
}));
|
|
21799
22090
|
};
|
|
21800
22091
|
});
|
|
21801
|
-
registerTagTools(targetServer,
|
|
21802
|
-
registerWikilinkFeedbackTools(targetServer,
|
|
21803
|
-
registerCorrectionTools(targetServer,
|
|
21804
|
-
registerInitTools(targetServer,
|
|
22092
|
+
registerTagTools(targetServer, gvi, gvp);
|
|
22093
|
+
registerWikilinkFeedbackTools(targetServer, gsd);
|
|
22094
|
+
registerCorrectionTools(targetServer, gsd);
|
|
22095
|
+
registerInitTools(targetServer, gvp, gsd);
|
|
21805
22096
|
registerConfigTools(
|
|
21806
22097
|
targetServer,
|
|
21807
|
-
|
|
22098
|
+
gcf,
|
|
21808
22099
|
(newConfig) => {
|
|
21809
22100
|
flywheelConfig = newConfig;
|
|
21810
22101
|
setWikilinkConfig(newConfig);
|
|
21811
22102
|
},
|
|
21812
|
-
|
|
22103
|
+
gsd
|
|
21813
22104
|
);
|
|
21814
|
-
registerMetricsTools(targetServer,
|
|
21815
|
-
registerActivityTools(targetServer,
|
|
22105
|
+
registerMetricsTools(targetServer, gvi, gsd);
|
|
22106
|
+
registerActivityTools(targetServer, gsd, () => {
|
|
21816
22107
|
try {
|
|
21817
22108
|
return getSessionId();
|
|
21818
22109
|
} catch {
|
|
21819
22110
|
return null;
|
|
21820
22111
|
}
|
|
21821
22112
|
});
|
|
21822
|
-
registerSimilarityTools(targetServer,
|
|
21823
|
-
registerSemanticTools(targetServer,
|
|
21824
|
-
registerMergeTools2(targetServer,
|
|
21825
|
-
registerTemporalAnalysisTools(targetServer,
|
|
21826
|
-
registerMemoryTools(targetServer,
|
|
21827
|
-
registerRecallTools(targetServer,
|
|
21828
|
-
registerBriefTools(targetServer,
|
|
21829
|
-
registerVaultResources(targetServer, () =>
|
|
22113
|
+
registerSimilarityTools(targetServer, gvi, gvp, gsd);
|
|
22114
|
+
registerSemanticTools(targetServer, gvp, gsd);
|
|
22115
|
+
registerMergeTools2(targetServer, gsd);
|
|
22116
|
+
registerTemporalAnalysisTools(targetServer, gvi, gvp, gsd);
|
|
22117
|
+
registerMemoryTools(targetServer, gsd);
|
|
22118
|
+
registerRecallTools(targetServer, gsd, gvp, () => gvi() ?? null);
|
|
22119
|
+
registerBriefTools(targetServer, gsd);
|
|
22120
|
+
registerVaultResources(targetServer, () => gvi() ?? null);
|
|
21830
22121
|
}
|
|
21831
22122
|
function createConfiguredServer() {
|
|
21832
22123
|
const s = new McpServer(
|
|
21833
22124
|
{ name: "flywheel-memory", version: pkg.version },
|
|
21834
22125
|
{ instructions: generateInstructions(enabledCategories, vaultRegistry) }
|
|
21835
22126
|
);
|
|
21836
|
-
applyToolGating(s, enabledCategories, () => stateDb, vaultRegistry);
|
|
22127
|
+
applyToolGating(s, enabledCategories, () => stateDb, vaultRegistry, () => vaultPath);
|
|
21837
22128
|
registerAllTools(s);
|
|
21838
22129
|
return s;
|
|
21839
22130
|
}
|
|
@@ -21841,7 +22132,7 @@ var server = new McpServer(
|
|
|
21841
22132
|
{ name: "flywheel-memory", version: pkg.version },
|
|
21842
22133
|
{ instructions: generateInstructions(enabledCategories, vaultRegistry) }
|
|
21843
22134
|
);
|
|
21844
|
-
var _gatingResult = applyToolGating(server, enabledCategories, () => stateDb, vaultRegistry);
|
|
22135
|
+
var _gatingResult = applyToolGating(server, enabledCategories, () => stateDb, vaultRegistry, () => vaultPath);
|
|
21845
22136
|
registerAllTools(server);
|
|
21846
22137
|
var categoryList = Array.from(enabledCategories).sort().join(", ");
|
|
21847
22138
|
serverLog("server", `Tool categories: ${categoryList}`);
|
|
@@ -21875,6 +22166,20 @@ async function initializeVault(name, vaultPathArg) {
|
|
|
21875
22166
|
}
|
|
21876
22167
|
return ctx;
|
|
21877
22168
|
}
|
|
22169
|
+
function buildVaultScope(ctx) {
|
|
22170
|
+
return {
|
|
22171
|
+
name: ctx.name,
|
|
22172
|
+
vaultPath: ctx.vaultPath,
|
|
22173
|
+
stateDb: ctx.stateDb,
|
|
22174
|
+
flywheelConfig: ctx.flywheelConfig,
|
|
22175
|
+
vaultIndex: ctx.vaultIndex,
|
|
22176
|
+
cooccurrenceIndex: ctx.cooccurrenceIndex,
|
|
22177
|
+
indexState: ctx.indexState,
|
|
22178
|
+
indexError: ctx.indexError,
|
|
22179
|
+
embeddingsBuilding: ctx.embeddingsBuilding,
|
|
22180
|
+
entityEmbeddingsMap: getEntityEmbeddingsMap()
|
|
22181
|
+
};
|
|
22182
|
+
}
|
|
21878
22183
|
function activateVault(ctx) {
|
|
21879
22184
|
globalThis.__flywheel_active_vault = ctx.name;
|
|
21880
22185
|
if (ctx.stateDb) {
|
|
@@ -21891,16 +22196,7 @@ function activateVault(ctx) {
|
|
|
21891
22196
|
setIndexState(ctx.indexState);
|
|
21892
22197
|
setIndexError(ctx.indexError);
|
|
21893
22198
|
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
|
-
});
|
|
22199
|
+
setActiveScope(buildVaultScope(ctx));
|
|
21904
22200
|
}
|
|
21905
22201
|
function getActiveVaultContext() {
|
|
21906
22202
|
if (!vaultRegistry) return null;
|
|
@@ -21967,6 +22263,10 @@ async function main() {
|
|
|
21967
22263
|
const { createMcpExpressApp } = await import("@modelcontextprotocol/sdk/server/express.js");
|
|
21968
22264
|
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
21969
22265
|
const httpPort = parseInt(process.env.FLYWHEEL_HTTP_PORT ?? "3111", 10);
|
|
22266
|
+
if (!Number.isFinite(httpPort) || httpPort < 1 || httpPort > 65535) {
|
|
22267
|
+
console.error(`[flywheel] Fatal: invalid FLYWHEEL_HTTP_PORT: ${process.env.FLYWHEEL_HTTP_PORT} (must be 1-65535)`);
|
|
22268
|
+
process.exit(1);
|
|
22269
|
+
}
|
|
21970
22270
|
const httpHost = process.env.FLYWHEEL_HTTP_HOST ?? "127.0.0.1";
|
|
21971
22271
|
const app = createMcpExpressApp({ host: httpHost });
|
|
21972
22272
|
app.post("/mcp", async (req, res) => {
|
|
@@ -22139,6 +22439,7 @@ function runPeriodicMaintenance(db4) {
|
|
|
22139
22439
|
purgeOldSuggestionEvents(db4, 30);
|
|
22140
22440
|
purgeOldNoteLinkHistory(db4, 90);
|
|
22141
22441
|
purgeOldSnapshots(db4, 90);
|
|
22442
|
+
pruneStaleRetrievalCooccurrence(db4, 30);
|
|
22142
22443
|
lastPurgeAt = now;
|
|
22143
22444
|
serverLog("server", "Daily purge complete");
|
|
22144
22445
|
}
|
|
@@ -23012,6 +23313,21 @@ async function runPostIndexWork(index) {
|
|
|
23012
23313
|
tracker.end({ error: String(e) });
|
|
23013
23314
|
serverLog("watcher", `Tag scan: failed: ${e}`, "error");
|
|
23014
23315
|
}
|
|
23316
|
+
tracker.start("retrieval_cooccurrence", {});
|
|
23317
|
+
try {
|
|
23318
|
+
if (stateDb) {
|
|
23319
|
+
const inserted = mineRetrievalCooccurrence(stateDb);
|
|
23320
|
+
tracker.end({ pairs_inserted: inserted });
|
|
23321
|
+
if (inserted > 0) {
|
|
23322
|
+
serverLog("watcher", `Retrieval co-occurrence: ${inserted} new pairs`);
|
|
23323
|
+
}
|
|
23324
|
+
} else {
|
|
23325
|
+
tracker.end({ skipped: "no stateDb" });
|
|
23326
|
+
}
|
|
23327
|
+
} catch (e) {
|
|
23328
|
+
tracker.end({ error: String(e) });
|
|
23329
|
+
serverLog("watcher", `Retrieval co-occurrence: failed: ${e}`, "error");
|
|
23330
|
+
}
|
|
23015
23331
|
const duration = Date.now() - batchStart;
|
|
23016
23332
|
if (stateDb) {
|
|
23017
23333
|
recordIndexEvent(stateDb, {
|