@velvetmonkey/flywheel-memory 2.0.49 → 2.0.51
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 +1291 -243
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7381,7 +7381,7 @@ function refreshIfStale(vaultPath2, index, excludeTags) {
|
|
|
7381
7381
|
}
|
|
7382
7382
|
|
|
7383
7383
|
// src/index.ts
|
|
7384
|
-
import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb3, findEntityMatches as findEntityMatches2, getProtectedZones as getProtectedZones2, rangeOverlapsProtectedZone } from "@velvetmonkey/vault-core";
|
|
7384
|
+
import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb3, findEntityMatches as findEntityMatches2, getProtectedZones as getProtectedZones2, rangeOverlapsProtectedZone, detectImplicitEntities as detectImplicitEntities3 } from "@velvetmonkey/vault-core";
|
|
7385
7385
|
|
|
7386
7386
|
// src/tools/read/graph.ts
|
|
7387
7387
|
import * as fs9 from "fs";
|
|
@@ -7909,6 +7909,7 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
7909
7909
|
|
|
7910
7910
|
// src/tools/read/wikilinks.ts
|
|
7911
7911
|
import { z as z2 } from "zod";
|
|
7912
|
+
import { detectImplicitEntities as detectImplicitEntities2 } from "@velvetmonkey/vault-core";
|
|
7912
7913
|
function findEntityMatches(text, entities) {
|
|
7913
7914
|
const matches = [];
|
|
7914
7915
|
const sortedEntities = Array.from(entities.entries()).filter(([name]) => name.length >= 2).sort((a, b) => b[0].length - a[0].length);
|
|
@@ -8014,28 +8015,83 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8014
8015
|
text: z2.string().describe("The text to analyze for potential wikilinks"),
|
|
8015
8016
|
limit: z2.coerce.number().default(50).describe("Maximum number of suggestions to return"),
|
|
8016
8017
|
offset: z2.coerce.number().default(0).describe("Number of suggestions to skip (for pagination)"),
|
|
8017
|
-
detail: z2.boolean().default(false).describe("Include per-layer score breakdown for each suggestion")
|
|
8018
|
-
note_path: z2.string().optional().describe("Path of the note being analyzed (enables strictness override for daily notes)")
|
|
8018
|
+
detail: z2.boolean().default(false).describe("Include per-layer score breakdown for each suggestion")
|
|
8019
8019
|
},
|
|
8020
8020
|
outputSchema: SuggestWikilinksOutputSchema
|
|
8021
8021
|
},
|
|
8022
|
-
async ({ text, limit: requestedLimit, offset, detail
|
|
8022
|
+
async ({ text, limit: requestedLimit, offset, detail }) => {
|
|
8023
8023
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
8024
8024
|
const index = getIndex();
|
|
8025
8025
|
const allMatches = findEntityMatches(text, index.entities);
|
|
8026
8026
|
const matches = allMatches.slice(offset, offset + limit);
|
|
8027
|
+
const linkedSet = new Set(allMatches.map((m) => m.entity.toLowerCase()));
|
|
8028
|
+
const prospects = [];
|
|
8029
|
+
const prospectSeen = /* @__PURE__ */ new Set();
|
|
8030
|
+
for (const [target, links] of index.backlinks) {
|
|
8031
|
+
if (links.length < 2) continue;
|
|
8032
|
+
if (index.entities.has(target.toLowerCase())) continue;
|
|
8033
|
+
if (linkedSet.has(target.toLowerCase())) continue;
|
|
8034
|
+
const targetLower = target.toLowerCase();
|
|
8035
|
+
const textLower = text.toLowerCase();
|
|
8036
|
+
let searchPos = 0;
|
|
8037
|
+
while (searchPos < textLower.length) {
|
|
8038
|
+
const pos = textLower.indexOf(targetLower, searchPos);
|
|
8039
|
+
if (pos === -1) break;
|
|
8040
|
+
const end = pos + target.length;
|
|
8041
|
+
const before = pos > 0 ? text[pos - 1] : " ";
|
|
8042
|
+
const after = end < text.length ? text[end] : " ";
|
|
8043
|
+
if (/[\s\n\r.,;:!?()[\]{}'"<>-]/.test(before) && /[\s\n\r.,;:!?()[\]{}'"<>-]/.test(after)) {
|
|
8044
|
+
if (!prospectSeen.has(targetLower)) {
|
|
8045
|
+
prospectSeen.add(targetLower);
|
|
8046
|
+
prospects.push({
|
|
8047
|
+
entity: text.substring(pos, end),
|
|
8048
|
+
start: pos,
|
|
8049
|
+
end,
|
|
8050
|
+
source: "dead_link",
|
|
8051
|
+
confidence: links.length >= 3 ? "high" : "medium",
|
|
8052
|
+
backlink_count: links.length
|
|
8053
|
+
});
|
|
8054
|
+
}
|
|
8055
|
+
break;
|
|
8056
|
+
}
|
|
8057
|
+
searchPos = pos + 1;
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
8060
|
+
const implicit = detectImplicitEntities2(text);
|
|
8061
|
+
for (const imp of implicit) {
|
|
8062
|
+
const impLower = imp.text.toLowerCase();
|
|
8063
|
+
if (linkedSet.has(impLower)) continue;
|
|
8064
|
+
if (prospectSeen.has(impLower)) {
|
|
8065
|
+
const existing = prospects.find((p) => p.entity.toLowerCase() === impLower);
|
|
8066
|
+
if (existing) {
|
|
8067
|
+
existing.source = "both";
|
|
8068
|
+
existing.confidence = "high";
|
|
8069
|
+
}
|
|
8070
|
+
continue;
|
|
8071
|
+
}
|
|
8072
|
+
prospectSeen.add(impLower);
|
|
8073
|
+
prospects.push({
|
|
8074
|
+
entity: imp.text,
|
|
8075
|
+
start: imp.start,
|
|
8076
|
+
end: imp.end,
|
|
8077
|
+
source: "implicit",
|
|
8078
|
+
confidence: "low"
|
|
8079
|
+
});
|
|
8080
|
+
}
|
|
8027
8081
|
const output = {
|
|
8028
8082
|
input_length: text.length,
|
|
8029
8083
|
suggestion_count: allMatches.length,
|
|
8030
8084
|
returned_count: matches.length,
|
|
8031
8085
|
suggestions: matches
|
|
8032
8086
|
};
|
|
8087
|
+
if (prospects.length > 0) {
|
|
8088
|
+
output.prospects = prospects;
|
|
8089
|
+
}
|
|
8033
8090
|
if (detail) {
|
|
8034
8091
|
const scored = await suggestRelatedLinks(text, {
|
|
8035
8092
|
detail: true,
|
|
8036
8093
|
maxSuggestions: limit,
|
|
8037
|
-
strictness: "balanced"
|
|
8038
|
-
...note_path ? { notePath: note_path } : {}
|
|
8094
|
+
strictness: "balanced"
|
|
8039
8095
|
});
|
|
8040
8096
|
if (scored.detailed) {
|
|
8041
8097
|
output.scored_suggestions = scored.detailed;
|
|
@@ -16334,87 +16390,1138 @@ function registerCorrectionTools(server2, getStateDb) {
|
|
|
16334
16390
|
);
|
|
16335
16391
|
}
|
|
16336
16392
|
|
|
16337
|
-
// src/tools/write/
|
|
16393
|
+
// src/tools/write/memory.ts
|
|
16338
16394
|
import { z as z23 } from "zod";
|
|
16339
|
-
|
|
16340
|
-
|
|
16341
|
-
|
|
16342
|
-
|
|
16343
|
-
|
|
16344
|
-
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16395
|
+
|
|
16396
|
+
// src/core/write/memory.ts
|
|
16397
|
+
import { recordEntityMention as recordEntityMention2 } from "@velvetmonkey/vault-core";
|
|
16398
|
+
function detectEntities(stateDb2, text) {
|
|
16399
|
+
const allEntities = stateDb2.getAllEntities.all();
|
|
16400
|
+
const detected = /* @__PURE__ */ new Set();
|
|
16401
|
+
const textLower = text.toLowerCase();
|
|
16402
|
+
for (const entity of allEntities) {
|
|
16403
|
+
if (textLower.includes(entity.name_lower)) {
|
|
16404
|
+
detected.add(entity.name);
|
|
16405
|
+
}
|
|
16406
|
+
if (entity.aliases_json) {
|
|
16407
|
+
try {
|
|
16408
|
+
const aliases = JSON.parse(entity.aliases_json);
|
|
16409
|
+
for (const alias of aliases) {
|
|
16410
|
+
if (alias.length >= 3 && textLower.includes(alias.toLowerCase())) {
|
|
16411
|
+
detected.add(entity.name);
|
|
16412
|
+
break;
|
|
16413
|
+
}
|
|
16414
|
+
}
|
|
16415
|
+
} catch {
|
|
16416
|
+
}
|
|
16417
|
+
}
|
|
16418
|
+
}
|
|
16419
|
+
return [...detected];
|
|
16420
|
+
}
|
|
16421
|
+
function updateGraphSignals(stateDb2, memoryKey, entities) {
|
|
16422
|
+
if (entities.length === 0) return;
|
|
16423
|
+
const now = /* @__PURE__ */ new Date();
|
|
16424
|
+
for (const entity of entities) {
|
|
16425
|
+
recordEntityMention2(stateDb2, entity, now);
|
|
16426
|
+
}
|
|
16427
|
+
const sourcePath = `memory:${memoryKey}`;
|
|
16428
|
+
const targets = new Set(entities.map((e) => e.toLowerCase()));
|
|
16429
|
+
updateStoredNoteLinks(stateDb2, sourcePath, targets);
|
|
16430
|
+
}
|
|
16431
|
+
function removeGraphSignals(stateDb2, memoryKey) {
|
|
16432
|
+
const sourcePath = `memory:${memoryKey}`;
|
|
16433
|
+
updateStoredNoteLinks(stateDb2, sourcePath, /* @__PURE__ */ new Set());
|
|
16434
|
+
}
|
|
16435
|
+
function storeMemory(stateDb2, options) {
|
|
16436
|
+
const {
|
|
16437
|
+
key,
|
|
16438
|
+
value,
|
|
16439
|
+
type,
|
|
16440
|
+
entity,
|
|
16441
|
+
confidence = 1,
|
|
16442
|
+
ttl_days,
|
|
16443
|
+
agent_id,
|
|
16444
|
+
session_id,
|
|
16445
|
+
visibility = "shared"
|
|
16446
|
+
} = options;
|
|
16447
|
+
const now = Date.now();
|
|
16448
|
+
const detectedEntities = detectEntities(stateDb2, value);
|
|
16449
|
+
if (entity && !detectedEntities.includes(entity)) {
|
|
16450
|
+
detectedEntities.push(entity);
|
|
16451
|
+
}
|
|
16452
|
+
const entitiesJson = detectedEntities.length > 0 ? JSON.stringify(detectedEntities) : null;
|
|
16453
|
+
const existing = stateDb2.db.prepare(
|
|
16454
|
+
"SELECT id FROM memories WHERE key = ?"
|
|
16455
|
+
).get(key);
|
|
16456
|
+
if (existing) {
|
|
16457
|
+
stateDb2.db.prepare(`
|
|
16458
|
+
UPDATE memories SET
|
|
16459
|
+
value = ?, memory_type = ?, entity = ?, entities_json = ?,
|
|
16460
|
+
source_agent_id = ?, source_session_id = ?,
|
|
16461
|
+
confidence = ?, updated_at = ?, accessed_at = ?,
|
|
16462
|
+
ttl_days = ?, visibility = ?, superseded_by = NULL
|
|
16463
|
+
WHERE key = ?
|
|
16464
|
+
`).run(
|
|
16465
|
+
value,
|
|
16466
|
+
type,
|
|
16467
|
+
entity ?? null,
|
|
16468
|
+
entitiesJson,
|
|
16469
|
+
agent_id ?? null,
|
|
16470
|
+
session_id ?? null,
|
|
16471
|
+
confidence,
|
|
16472
|
+
now,
|
|
16473
|
+
now,
|
|
16474
|
+
ttl_days ?? null,
|
|
16475
|
+
visibility,
|
|
16476
|
+
key
|
|
16477
|
+
);
|
|
16478
|
+
} else {
|
|
16479
|
+
stateDb2.db.prepare(`
|
|
16480
|
+
INSERT INTO memories (key, value, memory_type, entity, entities_json,
|
|
16481
|
+
source_agent_id, source_session_id, confidence,
|
|
16482
|
+
created_at, updated_at, accessed_at, ttl_days, visibility)
|
|
16483
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
16484
|
+
`).run(
|
|
16485
|
+
key,
|
|
16486
|
+
value,
|
|
16487
|
+
type,
|
|
16488
|
+
entity ?? null,
|
|
16489
|
+
entitiesJson,
|
|
16490
|
+
agent_id ?? null,
|
|
16491
|
+
session_id ?? null,
|
|
16492
|
+
confidence,
|
|
16493
|
+
now,
|
|
16494
|
+
now,
|
|
16495
|
+
now,
|
|
16496
|
+
ttl_days ?? null,
|
|
16497
|
+
visibility
|
|
16498
|
+
);
|
|
16499
|
+
}
|
|
16500
|
+
updateGraphSignals(stateDb2, key, detectedEntities);
|
|
16501
|
+
return stateDb2.db.prepare(
|
|
16502
|
+
"SELECT * FROM memories WHERE key = ?"
|
|
16503
|
+
).get(key);
|
|
16504
|
+
}
|
|
16505
|
+
function getMemory(stateDb2, key) {
|
|
16506
|
+
const memory = stateDb2.db.prepare(
|
|
16507
|
+
"SELECT * FROM memories WHERE key = ? AND superseded_by IS NULL"
|
|
16508
|
+
).get(key);
|
|
16509
|
+
if (!memory) return null;
|
|
16510
|
+
stateDb2.db.prepare(
|
|
16511
|
+
"UPDATE memories SET accessed_at = ? WHERE id = ?"
|
|
16512
|
+
).run(Date.now(), memory.id);
|
|
16513
|
+
return memory;
|
|
16514
|
+
}
|
|
16515
|
+
function searchMemories(stateDb2, options) {
|
|
16516
|
+
const { query, type, entity, limit = 20, agent_id } = options;
|
|
16517
|
+
const conditions = ["m.superseded_by IS NULL"];
|
|
16518
|
+
const params = [];
|
|
16519
|
+
if (type) {
|
|
16520
|
+
conditions.push("m.memory_type = ?");
|
|
16521
|
+
params.push(type);
|
|
16522
|
+
}
|
|
16523
|
+
if (entity) {
|
|
16524
|
+
conditions.push("(m.entity = ? COLLATE NOCASE OR m.entities_json LIKE ?)");
|
|
16525
|
+
params.push(entity, `%"${entity}"%`);
|
|
16526
|
+
}
|
|
16527
|
+
if (agent_id) {
|
|
16528
|
+
conditions.push("(m.visibility = 'shared' OR m.source_agent_id = ?)");
|
|
16529
|
+
params.push(agent_id);
|
|
16530
|
+
}
|
|
16531
|
+
const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
|
|
16532
|
+
try {
|
|
16533
|
+
const results = stateDb2.db.prepare(`
|
|
16534
|
+
SELECT m.* FROM memories_fts
|
|
16535
|
+
JOIN memories m ON m.id = memories_fts.rowid
|
|
16536
|
+
WHERE memories_fts MATCH ?
|
|
16537
|
+
${where}
|
|
16538
|
+
ORDER BY bm25(memories_fts)
|
|
16539
|
+
LIMIT ?
|
|
16540
|
+
`).all(query, ...params, limit);
|
|
16541
|
+
return results;
|
|
16542
|
+
} catch (err) {
|
|
16543
|
+
if (err instanceof Error && err.message.includes("fts5: syntax error")) {
|
|
16544
|
+
throw new Error(`Invalid search query: ${query}. Check FTS5 syntax.`);
|
|
16545
|
+
}
|
|
16546
|
+
throw err;
|
|
16547
|
+
}
|
|
16548
|
+
}
|
|
16549
|
+
function listMemories(stateDb2, options = {}) {
|
|
16550
|
+
const { type, entity, limit = 50, agent_id, include_expired = false } = options;
|
|
16551
|
+
const conditions = [];
|
|
16552
|
+
const params = [];
|
|
16553
|
+
if (!include_expired) {
|
|
16554
|
+
conditions.push("superseded_by IS NULL");
|
|
16555
|
+
}
|
|
16556
|
+
if (type) {
|
|
16557
|
+
conditions.push("memory_type = ?");
|
|
16558
|
+
params.push(type);
|
|
16559
|
+
}
|
|
16560
|
+
if (entity) {
|
|
16561
|
+
conditions.push("(entity = ? COLLATE NOCASE OR entities_json LIKE ?)");
|
|
16562
|
+
params.push(entity, `%"${entity}"%`);
|
|
16563
|
+
}
|
|
16564
|
+
if (agent_id) {
|
|
16565
|
+
conditions.push("(visibility = 'shared' OR source_agent_id = ?)");
|
|
16566
|
+
params.push(agent_id);
|
|
16567
|
+
}
|
|
16568
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
16569
|
+
params.push(limit);
|
|
16570
|
+
return stateDb2.db.prepare(
|
|
16571
|
+
`SELECT * FROM memories ${where} ORDER BY updated_at DESC LIMIT ?`
|
|
16572
|
+
).all(...params);
|
|
16573
|
+
}
|
|
16574
|
+
function forgetMemory(stateDb2, key) {
|
|
16575
|
+
const memory = stateDb2.db.prepare(
|
|
16576
|
+
"SELECT id FROM memories WHERE key = ?"
|
|
16577
|
+
).get(key);
|
|
16578
|
+
if (!memory) return false;
|
|
16579
|
+
removeGraphSignals(stateDb2, key);
|
|
16580
|
+
stateDb2.db.prepare("DELETE FROM memories WHERE key = ?").run(key);
|
|
16581
|
+
return true;
|
|
16582
|
+
}
|
|
16583
|
+
function storeSessionSummary(stateDb2, sessionId, summary, options = {}) {
|
|
16584
|
+
const now = Date.now();
|
|
16585
|
+
const { topics, notes_modified, agent_id, started_at, tool_count } = options;
|
|
16586
|
+
stateDb2.db.prepare(`
|
|
16587
|
+
INSERT OR REPLACE INTO session_summaries
|
|
16588
|
+
(session_id, summary, topics_json, notes_modified_json,
|
|
16589
|
+
agent_id, started_at, ended_at, tool_count)
|
|
16590
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
16591
|
+
`).run(
|
|
16592
|
+
sessionId,
|
|
16593
|
+
summary,
|
|
16594
|
+
topics ? JSON.stringify(topics) : null,
|
|
16595
|
+
notes_modified ? JSON.stringify(notes_modified) : null,
|
|
16596
|
+
agent_id ?? null,
|
|
16597
|
+
started_at ?? null,
|
|
16598
|
+
now,
|
|
16599
|
+
tool_count ?? null
|
|
16600
|
+
);
|
|
16601
|
+
return stateDb2.db.prepare(
|
|
16602
|
+
"SELECT * FROM session_summaries WHERE session_id = ?"
|
|
16603
|
+
).get(sessionId);
|
|
16604
|
+
}
|
|
16605
|
+
function getRecentSessionSummaries(stateDb2, limit = 5, agent_id) {
|
|
16606
|
+
if (agent_id) {
|
|
16607
|
+
return stateDb2.db.prepare(
|
|
16608
|
+
"SELECT * FROM session_summaries WHERE agent_id = ? ORDER BY ended_at DESC LIMIT ?"
|
|
16609
|
+
).all(agent_id, limit);
|
|
16610
|
+
}
|
|
16611
|
+
return stateDb2.db.prepare(
|
|
16612
|
+
"SELECT * FROM session_summaries ORDER BY ended_at DESC LIMIT ?"
|
|
16613
|
+
).all(limit);
|
|
16614
|
+
}
|
|
16615
|
+
function findContradictions2(stateDb2, entity) {
|
|
16616
|
+
const conditions = ["superseded_by IS NULL"];
|
|
16617
|
+
const params = [];
|
|
16618
|
+
if (entity) {
|
|
16619
|
+
conditions.push("(entity = ? COLLATE NOCASE OR entities_json LIKE ?)");
|
|
16620
|
+
params.push(entity, `%"${entity}"%`);
|
|
16621
|
+
}
|
|
16622
|
+
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
16623
|
+
const memories = stateDb2.db.prepare(
|
|
16624
|
+
`SELECT * FROM memories ${where} ORDER BY entity, key, updated_at DESC`
|
|
16625
|
+
).all(...params);
|
|
16626
|
+
const contradictions = [];
|
|
16627
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
16628
|
+
for (const m of memories) {
|
|
16629
|
+
const group = m.key;
|
|
16630
|
+
const list = byKey.get(group) || [];
|
|
16631
|
+
list.push(m);
|
|
16632
|
+
byKey.set(group, list);
|
|
16633
|
+
}
|
|
16634
|
+
for (const [, mems] of byKey) {
|
|
16635
|
+
if (mems.length > 1) {
|
|
16636
|
+
for (let i = 0; i < mems.length - 1; i++) {
|
|
16637
|
+
contradictions.push({ memory_a: mems[i], memory_b: mems[i + 1] });
|
|
16350
16638
|
}
|
|
16639
|
+
}
|
|
16640
|
+
}
|
|
16641
|
+
return contradictions;
|
|
16642
|
+
}
|
|
16643
|
+
|
|
16644
|
+
// src/tools/write/memory.ts
|
|
16645
|
+
function registerMemoryTools(server2, getStateDb) {
|
|
16646
|
+
server2.tool(
|
|
16647
|
+
"memory",
|
|
16648
|
+
"Store, retrieve, search, and manage agent working memory. Actions: store, get, search, list, forget, summarize_session.",
|
|
16649
|
+
{
|
|
16650
|
+
action: z23.enum(["store", "get", "search", "list", "forget", "summarize_session"]).describe("Action to perform"),
|
|
16651
|
+
// store params
|
|
16652
|
+
key: z23.string().optional().describe('Memory key (e.g., "user.pref.theme", "project.x.deadline")'),
|
|
16653
|
+
value: z23.string().optional().describe("The fact/preference/observation to store (up to 2000 chars)"),
|
|
16654
|
+
type: z23.enum(["fact", "preference", "observation", "summary"]).optional().describe("Memory type"),
|
|
16655
|
+
entity: z23.string().optional().describe("Primary entity association"),
|
|
16656
|
+
confidence: z23.number().min(0).max(1).optional().describe("Confidence level (0-1, default 1.0)"),
|
|
16657
|
+
ttl_days: z23.number().min(1).optional().describe("Time-to-live in days (null = permanent)"),
|
|
16658
|
+
// search params
|
|
16659
|
+
query: z23.string().optional().describe("FTS5 search query"),
|
|
16660
|
+
// list/search params
|
|
16661
|
+
limit: z23.number().min(1).max(200).optional().describe("Max results to return"),
|
|
16662
|
+
// summarize_session params
|
|
16663
|
+
session_id: z23.string().optional().describe("Session ID for summarize_session"),
|
|
16664
|
+
summary: z23.string().optional().describe("Session summary text"),
|
|
16665
|
+
topics: z23.array(z23.string()).optional().describe("Topics discussed in session"),
|
|
16666
|
+
notes_modified: z23.array(z23.string()).optional().describe("Note paths modified during session"),
|
|
16667
|
+
tool_count: z23.number().optional().describe("Number of tool calls in session")
|
|
16351
16668
|
},
|
|
16352
|
-
async (
|
|
16353
|
-
|
|
16669
|
+
async (args) => {
|
|
16670
|
+
const stateDb2 = getStateDb();
|
|
16671
|
+
if (!stateDb2) {
|
|
16672
|
+
return {
|
|
16673
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
16674
|
+
isError: true
|
|
16675
|
+
};
|
|
16676
|
+
}
|
|
16677
|
+
const agentId = process.env.FLYWHEEL_AGENT_ID || void 0;
|
|
16678
|
+
let sessionId;
|
|
16679
|
+
try {
|
|
16680
|
+
const { getSessionId: getSessionId2 } = await import("@velvetmonkey/vault-core");
|
|
16681
|
+
sessionId = getSessionId2();
|
|
16682
|
+
} catch {
|
|
16683
|
+
}
|
|
16684
|
+
switch (args.action) {
|
|
16685
|
+
case "store": {
|
|
16686
|
+
if (!args.key || !args.value || !args.type) {
|
|
16687
|
+
return {
|
|
16688
|
+
content: [{ type: "text", text: JSON.stringify({ error: "store requires key, value, and type" }) }],
|
|
16689
|
+
isError: true
|
|
16690
|
+
};
|
|
16691
|
+
}
|
|
16692
|
+
if (args.value.length > 2e3) {
|
|
16693
|
+
return {
|
|
16694
|
+
content: [{ type: "text", text: JSON.stringify({ error: "value must be 2000 chars or less" }) }],
|
|
16695
|
+
isError: true
|
|
16696
|
+
};
|
|
16697
|
+
}
|
|
16698
|
+
const memory = storeMemory(stateDb2, {
|
|
16699
|
+
key: args.key,
|
|
16700
|
+
value: args.value,
|
|
16701
|
+
type: args.type,
|
|
16702
|
+
entity: args.entity,
|
|
16703
|
+
confidence: args.confidence,
|
|
16704
|
+
ttl_days: args.ttl_days,
|
|
16705
|
+
agent_id: agentId,
|
|
16706
|
+
session_id: sessionId
|
|
16707
|
+
});
|
|
16708
|
+
return {
|
|
16709
|
+
content: [{
|
|
16710
|
+
type: "text",
|
|
16711
|
+
text: JSON.stringify({
|
|
16712
|
+
stored: true,
|
|
16713
|
+
memory: {
|
|
16714
|
+
key: memory.key,
|
|
16715
|
+
value: memory.value,
|
|
16716
|
+
type: memory.memory_type,
|
|
16717
|
+
entity: memory.entity,
|
|
16718
|
+
entities_detected: memory.entities_json ? JSON.parse(memory.entities_json) : [],
|
|
16719
|
+
confidence: memory.confidence
|
|
16720
|
+
}
|
|
16721
|
+
}, null, 2)
|
|
16722
|
+
}]
|
|
16723
|
+
};
|
|
16724
|
+
}
|
|
16354
16725
|
case "get": {
|
|
16355
|
-
|
|
16726
|
+
if (!args.key) {
|
|
16727
|
+
return {
|
|
16728
|
+
content: [{ type: "text", text: JSON.stringify({ error: "get requires key" }) }],
|
|
16729
|
+
isError: true
|
|
16730
|
+
};
|
|
16731
|
+
}
|
|
16732
|
+
const memory = getMemory(stateDb2, args.key);
|
|
16733
|
+
if (!memory) {
|
|
16734
|
+
return {
|
|
16735
|
+
content: [{ type: "text", text: JSON.stringify({ found: false, key: args.key }) }]
|
|
16736
|
+
};
|
|
16737
|
+
}
|
|
16356
16738
|
return {
|
|
16357
|
-
content: [{
|
|
16739
|
+
content: [{
|
|
16740
|
+
type: "text",
|
|
16741
|
+
text: JSON.stringify({
|
|
16742
|
+
found: true,
|
|
16743
|
+
memory: {
|
|
16744
|
+
key: memory.key,
|
|
16745
|
+
value: memory.value,
|
|
16746
|
+
type: memory.memory_type,
|
|
16747
|
+
entity: memory.entity,
|
|
16748
|
+
entities: memory.entities_json ? JSON.parse(memory.entities_json) : [],
|
|
16749
|
+
confidence: memory.confidence,
|
|
16750
|
+
created_at: memory.created_at,
|
|
16751
|
+
updated_at: memory.updated_at,
|
|
16752
|
+
accessed_at: memory.accessed_at
|
|
16753
|
+
}
|
|
16754
|
+
}, null, 2)
|
|
16755
|
+
}]
|
|
16358
16756
|
};
|
|
16359
16757
|
}
|
|
16360
|
-
case "
|
|
16361
|
-
if (!
|
|
16758
|
+
case "search": {
|
|
16759
|
+
if (!args.query) {
|
|
16362
16760
|
return {
|
|
16363
|
-
content: [{ type: "text", text: JSON.stringify({ error: "
|
|
16761
|
+
content: [{ type: "text", text: JSON.stringify({ error: "search requires query" }) }],
|
|
16762
|
+
isError: true
|
|
16364
16763
|
};
|
|
16365
16764
|
}
|
|
16366
|
-
const
|
|
16367
|
-
|
|
16765
|
+
const results = searchMemories(stateDb2, {
|
|
16766
|
+
query: args.query,
|
|
16767
|
+
type: args.type,
|
|
16768
|
+
entity: args.entity,
|
|
16769
|
+
limit: args.limit,
|
|
16770
|
+
agent_id: agentId
|
|
16771
|
+
});
|
|
16772
|
+
return {
|
|
16773
|
+
content: [{
|
|
16774
|
+
type: "text",
|
|
16775
|
+
text: JSON.stringify({
|
|
16776
|
+
results: results.map((m) => ({
|
|
16777
|
+
key: m.key,
|
|
16778
|
+
value: m.value,
|
|
16779
|
+
type: m.memory_type,
|
|
16780
|
+
entity: m.entity,
|
|
16781
|
+
confidence: m.confidence,
|
|
16782
|
+
updated_at: m.updated_at
|
|
16783
|
+
})),
|
|
16784
|
+
count: results.length
|
|
16785
|
+
}, null, 2)
|
|
16786
|
+
}]
|
|
16787
|
+
};
|
|
16788
|
+
}
|
|
16789
|
+
case "list": {
|
|
16790
|
+
const results = listMemories(stateDb2, {
|
|
16791
|
+
type: args.type,
|
|
16792
|
+
entity: args.entity,
|
|
16793
|
+
limit: args.limit,
|
|
16794
|
+
agent_id: agentId
|
|
16795
|
+
});
|
|
16796
|
+
return {
|
|
16797
|
+
content: [{
|
|
16798
|
+
type: "text",
|
|
16799
|
+
text: JSON.stringify({
|
|
16800
|
+
memories: results.map((m) => ({
|
|
16801
|
+
key: m.key,
|
|
16802
|
+
value: m.value,
|
|
16803
|
+
type: m.memory_type,
|
|
16804
|
+
entity: m.entity,
|
|
16805
|
+
confidence: m.confidence,
|
|
16806
|
+
updated_at: m.updated_at
|
|
16807
|
+
})),
|
|
16808
|
+
count: results.length
|
|
16809
|
+
}, null, 2)
|
|
16810
|
+
}]
|
|
16811
|
+
};
|
|
16812
|
+
}
|
|
16813
|
+
case "forget": {
|
|
16814
|
+
if (!args.key) {
|
|
16368
16815
|
return {
|
|
16369
|
-
content: [{ type: "text", text: JSON.stringify({ error: "
|
|
16816
|
+
content: [{ type: "text", text: JSON.stringify({ error: "forget requires key" }) }],
|
|
16817
|
+
isError: true
|
|
16370
16818
|
};
|
|
16371
16819
|
}
|
|
16372
|
-
const
|
|
16373
|
-
const updated = { ...current, [key]: value };
|
|
16374
|
-
saveFlywheelConfigToDb2(stateDb2, updated);
|
|
16375
|
-
const reloaded = loadConfig(stateDb2);
|
|
16376
|
-
setConfig(reloaded);
|
|
16820
|
+
const deleted = forgetMemory(stateDb2, args.key);
|
|
16377
16821
|
return {
|
|
16378
|
-
content: [{
|
|
16822
|
+
content: [{
|
|
16823
|
+
type: "text",
|
|
16824
|
+
text: JSON.stringify({ forgotten: deleted, key: args.key }, null, 2)
|
|
16825
|
+
}]
|
|
16826
|
+
};
|
|
16827
|
+
}
|
|
16828
|
+
case "summarize_session": {
|
|
16829
|
+
const sid = args.session_id || sessionId;
|
|
16830
|
+
if (!sid || !args.summary) {
|
|
16831
|
+
return {
|
|
16832
|
+
content: [{ type: "text", text: JSON.stringify({ error: "summarize_session requires session_id and summary" }) }],
|
|
16833
|
+
isError: true
|
|
16834
|
+
};
|
|
16835
|
+
}
|
|
16836
|
+
const result = storeSessionSummary(stateDb2, sid, args.summary, {
|
|
16837
|
+
topics: args.topics,
|
|
16838
|
+
notes_modified: args.notes_modified,
|
|
16839
|
+
agent_id: agentId,
|
|
16840
|
+
tool_count: args.tool_count
|
|
16841
|
+
});
|
|
16842
|
+
return {
|
|
16843
|
+
content: [{
|
|
16844
|
+
type: "text",
|
|
16845
|
+
text: JSON.stringify({
|
|
16846
|
+
stored: true,
|
|
16847
|
+
session_id: result.session_id,
|
|
16848
|
+
summary_length: result.summary.length
|
|
16849
|
+
}, null, 2)
|
|
16850
|
+
}]
|
|
16379
16851
|
};
|
|
16380
16852
|
}
|
|
16853
|
+
default:
|
|
16854
|
+
return {
|
|
16855
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${args.action}` }) }],
|
|
16856
|
+
isError: true
|
|
16857
|
+
};
|
|
16381
16858
|
}
|
|
16382
16859
|
}
|
|
16383
16860
|
);
|
|
16384
16861
|
}
|
|
16385
16862
|
|
|
16386
|
-
// src/tools/
|
|
16863
|
+
// src/tools/read/recall.ts
|
|
16387
16864
|
import { z as z24 } from "zod";
|
|
16388
|
-
import
|
|
16389
|
-
|
|
16390
|
-
|
|
16391
|
-
|
|
16392
|
-
const
|
|
16393
|
-
|
|
16394
|
-
const
|
|
16395
|
-
|
|
16865
|
+
import { searchEntities as searchEntitiesDb2 } from "@velvetmonkey/vault-core";
|
|
16866
|
+
function scoreTextRelevance(query, content) {
|
|
16867
|
+
const queryTokens = tokenize(query).map((t) => t.toLowerCase());
|
|
16868
|
+
const queryStems = queryTokens.map((t) => stem(t));
|
|
16869
|
+
const contentLower = content.toLowerCase();
|
|
16870
|
+
const contentTokens = new Set(tokenize(contentLower));
|
|
16871
|
+
const contentStems = new Set([...contentTokens].map((t) => stem(t)));
|
|
16872
|
+
let score = 0;
|
|
16873
|
+
for (let i = 0; i < queryTokens.length; i++) {
|
|
16874
|
+
const token = queryTokens[i];
|
|
16875
|
+
const stemmed = queryStems[i];
|
|
16876
|
+
if (contentTokens.has(token)) {
|
|
16877
|
+
score += 10;
|
|
16878
|
+
} else if (contentStems.has(stemmed)) {
|
|
16879
|
+
score += 5;
|
|
16880
|
+
}
|
|
16881
|
+
}
|
|
16882
|
+
if (contentLower.includes(query.toLowerCase())) {
|
|
16883
|
+
score += 15;
|
|
16884
|
+
}
|
|
16885
|
+
return score;
|
|
16396
16886
|
}
|
|
16397
|
-
|
|
16887
|
+
function getEdgeWeightBoost(entityName, edgeWeightMap) {
|
|
16888
|
+
const avgWeight = edgeWeightMap.get(entityName.toLowerCase());
|
|
16889
|
+
if (!avgWeight || avgWeight <= 1) return 0;
|
|
16890
|
+
return Math.min((avgWeight - 1) * 3, 6);
|
|
16891
|
+
}
|
|
16892
|
+
async function performRecall(stateDb2, query, options = {}) {
|
|
16893
|
+
const {
|
|
16894
|
+
max_results = 20,
|
|
16895
|
+
focus = "all",
|
|
16896
|
+
entity,
|
|
16897
|
+
max_tokens
|
|
16898
|
+
} = options;
|
|
16398
16899
|
const results = [];
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16405
|
-
|
|
16406
|
-
const
|
|
16407
|
-
|
|
16408
|
-
|
|
16409
|
-
|
|
16900
|
+
const recencyIndex2 = loadRecencyFromStateDb();
|
|
16901
|
+
const edgeWeightMap = getEntityEdgeWeightMap(stateDb2);
|
|
16902
|
+
const feedbackBoosts = getAllFeedbackBoosts(stateDb2);
|
|
16903
|
+
if (focus === "all" || focus === "entities") {
|
|
16904
|
+
try {
|
|
16905
|
+
const entityResults = searchEntitiesDb2(stateDb2, query, max_results);
|
|
16906
|
+
for (const e of entityResults) {
|
|
16907
|
+
const textScore = scoreTextRelevance(query, `${e.name} ${e.description || ""}`);
|
|
16908
|
+
const recency = recencyIndex2 ? getRecencyBoost(e.name, recencyIndex2) : 0;
|
|
16909
|
+
const feedback = feedbackBoosts.get(e.name) ?? 0;
|
|
16910
|
+
const edgeWeight = getEdgeWeightBoost(e.name, edgeWeightMap);
|
|
16911
|
+
const total = textScore + recency + feedback + edgeWeight;
|
|
16912
|
+
if (total > 0) {
|
|
16913
|
+
results.push({
|
|
16914
|
+
type: "entity",
|
|
16915
|
+
id: e.name,
|
|
16916
|
+
content: e.description || `Entity: ${e.name} (${e.category})`,
|
|
16917
|
+
score: total,
|
|
16918
|
+
breakdown: {
|
|
16919
|
+
textRelevance: textScore,
|
|
16920
|
+
recencyBoost: recency,
|
|
16921
|
+
cooccurrenceBoost: 0,
|
|
16922
|
+
feedbackBoost: feedback,
|
|
16923
|
+
edgeWeightBoost: edgeWeight,
|
|
16924
|
+
semanticBoost: 0
|
|
16925
|
+
}
|
|
16926
|
+
});
|
|
16927
|
+
}
|
|
16410
16928
|
}
|
|
16929
|
+
} catch {
|
|
16411
16930
|
}
|
|
16412
|
-
} catch {
|
|
16413
16931
|
}
|
|
16414
|
-
|
|
16932
|
+
if (focus === "all" || focus === "notes") {
|
|
16933
|
+
try {
|
|
16934
|
+
const noteResults = searchFTS5("", query, max_results);
|
|
16935
|
+
for (const n of noteResults) {
|
|
16936
|
+
const textScore = Math.max(10, scoreTextRelevance(query, `${n.title || ""} ${n.snippet || ""}`));
|
|
16937
|
+
results.push({
|
|
16938
|
+
type: "note",
|
|
16939
|
+
id: n.path,
|
|
16940
|
+
content: n.snippet || n.title || n.path,
|
|
16941
|
+
score: textScore,
|
|
16942
|
+
breakdown: {
|
|
16943
|
+
textRelevance: textScore,
|
|
16944
|
+
recencyBoost: 0,
|
|
16945
|
+
cooccurrenceBoost: 0,
|
|
16946
|
+
feedbackBoost: 0,
|
|
16947
|
+
edgeWeightBoost: 0,
|
|
16948
|
+
semanticBoost: 0
|
|
16949
|
+
}
|
|
16950
|
+
});
|
|
16951
|
+
}
|
|
16952
|
+
} catch {
|
|
16953
|
+
}
|
|
16954
|
+
}
|
|
16955
|
+
if (focus === "all" || focus === "memories") {
|
|
16956
|
+
try {
|
|
16957
|
+
const memResults = searchMemories(stateDb2, {
|
|
16958
|
+
query,
|
|
16959
|
+
entity,
|
|
16960
|
+
limit: max_results
|
|
16961
|
+
});
|
|
16962
|
+
for (const m of memResults) {
|
|
16963
|
+
const textScore = scoreTextRelevance(query, `${m.key} ${m.value}`);
|
|
16964
|
+
const memScore = textScore + m.confidence * 5;
|
|
16965
|
+
results.push({
|
|
16966
|
+
type: "memory",
|
|
16967
|
+
id: m.key,
|
|
16968
|
+
content: m.value,
|
|
16969
|
+
score: memScore,
|
|
16970
|
+
breakdown: {
|
|
16971
|
+
textRelevance: textScore,
|
|
16972
|
+
recencyBoost: 0,
|
|
16973
|
+
cooccurrenceBoost: 0,
|
|
16974
|
+
feedbackBoost: m.confidence * 5,
|
|
16975
|
+
edgeWeightBoost: 0,
|
|
16976
|
+
semanticBoost: 0
|
|
16977
|
+
}
|
|
16978
|
+
});
|
|
16979
|
+
}
|
|
16980
|
+
} catch {
|
|
16981
|
+
}
|
|
16982
|
+
}
|
|
16983
|
+
if ((focus === "all" || focus === "entities") && query.length >= 20 && hasEntityEmbeddingsIndex()) {
|
|
16984
|
+
try {
|
|
16985
|
+
const embedding = await embedTextCached(query);
|
|
16986
|
+
const semanticMatches = findSemanticallySimilarEntities(embedding, max_results);
|
|
16987
|
+
for (const match of semanticMatches) {
|
|
16988
|
+
if (match.similarity < 0.3) continue;
|
|
16989
|
+
const boost = match.similarity * 15;
|
|
16990
|
+
const existing = results.find((r) => r.type === "entity" && r.id === match.entityName);
|
|
16991
|
+
if (existing) {
|
|
16992
|
+
existing.score += boost;
|
|
16993
|
+
existing.breakdown.semanticBoost = boost;
|
|
16994
|
+
} else {
|
|
16995
|
+
results.push({
|
|
16996
|
+
type: "entity",
|
|
16997
|
+
id: match.entityName,
|
|
16998
|
+
content: `Semantically similar to: "${query}"`,
|
|
16999
|
+
score: boost,
|
|
17000
|
+
breakdown: {
|
|
17001
|
+
textRelevance: 0,
|
|
17002
|
+
recencyBoost: 0,
|
|
17003
|
+
cooccurrenceBoost: 0,
|
|
17004
|
+
feedbackBoost: 0,
|
|
17005
|
+
edgeWeightBoost: 0,
|
|
17006
|
+
semanticBoost: boost
|
|
17007
|
+
}
|
|
17008
|
+
});
|
|
17009
|
+
}
|
|
17010
|
+
}
|
|
17011
|
+
} catch {
|
|
17012
|
+
}
|
|
17013
|
+
}
|
|
17014
|
+
results.sort((a, b) => b.score - a.score);
|
|
17015
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17016
|
+
const deduped = results.filter((r) => {
|
|
17017
|
+
const key = `${r.type}:${r.id}`;
|
|
17018
|
+
if (seen.has(key)) return false;
|
|
17019
|
+
seen.add(key);
|
|
17020
|
+
return true;
|
|
17021
|
+
});
|
|
17022
|
+
const truncated = deduped.slice(0, max_results);
|
|
17023
|
+
if (max_tokens) {
|
|
17024
|
+
let tokenBudget = max_tokens;
|
|
17025
|
+
const budgeted = [];
|
|
17026
|
+
for (const r of truncated) {
|
|
17027
|
+
const estimatedTokens = Math.ceil(r.content.length / 4);
|
|
17028
|
+
if (tokenBudget - estimatedTokens < 0 && budgeted.length > 0) break;
|
|
17029
|
+
tokenBudget -= estimatedTokens;
|
|
17030
|
+
budgeted.push(r);
|
|
17031
|
+
}
|
|
17032
|
+
return budgeted;
|
|
17033
|
+
}
|
|
17034
|
+
return truncated;
|
|
16415
17035
|
}
|
|
16416
|
-
|
|
16417
|
-
|
|
17036
|
+
function registerRecallTools(server2, getStateDb) {
|
|
17037
|
+
server2.tool(
|
|
17038
|
+
"recall",
|
|
17039
|
+
"Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
|
|
17040
|
+
{
|
|
17041
|
+
query: z24.string().describe('What to recall (e.g., "Project X", "meetings about auth")'),
|
|
17042
|
+
max_results: z24.number().min(1).max(100).optional().describe("Max results (default: 20)"),
|
|
17043
|
+
focus: z24.enum(["entities", "notes", "memories", "all"]).optional().describe("Limit search to specific type (default: all)"),
|
|
17044
|
+
entity: z24.string().optional().describe("Filter memories by entity association"),
|
|
17045
|
+
max_tokens: z24.number().optional().describe("Token budget for response (truncates lower-ranked results)")
|
|
17046
|
+
},
|
|
17047
|
+
async (args) => {
|
|
17048
|
+
const stateDb2 = getStateDb();
|
|
17049
|
+
if (!stateDb2) {
|
|
17050
|
+
return {
|
|
17051
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
17052
|
+
isError: true
|
|
17053
|
+
};
|
|
17054
|
+
}
|
|
17055
|
+
const results = await performRecall(stateDb2, args.query, {
|
|
17056
|
+
max_results: args.max_results,
|
|
17057
|
+
focus: args.focus,
|
|
17058
|
+
entity: args.entity,
|
|
17059
|
+
max_tokens: args.max_tokens
|
|
17060
|
+
});
|
|
17061
|
+
const entities = results.filter((r) => r.type === "entity");
|
|
17062
|
+
const notes = results.filter((r) => r.type === "note");
|
|
17063
|
+
const memories = results.filter((r) => r.type === "memory");
|
|
17064
|
+
return {
|
|
17065
|
+
content: [{
|
|
17066
|
+
type: "text",
|
|
17067
|
+
text: JSON.stringify({
|
|
17068
|
+
query: args.query,
|
|
17069
|
+
total: results.length,
|
|
17070
|
+
entities: entities.map((e) => ({
|
|
17071
|
+
name: e.id,
|
|
17072
|
+
description: e.content,
|
|
17073
|
+
score: Math.round(e.score * 10) / 10,
|
|
17074
|
+
breakdown: e.breakdown
|
|
17075
|
+
})),
|
|
17076
|
+
notes: notes.map((n) => ({
|
|
17077
|
+
path: n.id,
|
|
17078
|
+
snippet: n.content,
|
|
17079
|
+
score: Math.round(n.score * 10) / 10
|
|
17080
|
+
})),
|
|
17081
|
+
memories: memories.map((m) => ({
|
|
17082
|
+
key: m.id,
|
|
17083
|
+
value: m.content,
|
|
17084
|
+
score: Math.round(m.score * 10) / 10
|
|
17085
|
+
}))
|
|
17086
|
+
}, null, 2)
|
|
17087
|
+
}]
|
|
17088
|
+
};
|
|
17089
|
+
}
|
|
17090
|
+
);
|
|
17091
|
+
}
|
|
17092
|
+
|
|
17093
|
+
// src/tools/read/brief.ts
|
|
17094
|
+
import { z as z25 } from "zod";
|
|
17095
|
+
|
|
17096
|
+
// src/core/shared/toolTracking.ts
|
|
17097
|
+
function recordToolInvocation(stateDb2, event) {
|
|
17098
|
+
stateDb2.db.prepare(
|
|
17099
|
+
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
|
|
17100
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
17101
|
+
).run(
|
|
17102
|
+
Date.now(),
|
|
17103
|
+
event.tool_name,
|
|
17104
|
+
event.session_id ?? null,
|
|
17105
|
+
event.note_paths ? JSON.stringify(event.note_paths) : null,
|
|
17106
|
+
event.duration_ms ?? null,
|
|
17107
|
+
event.success !== false ? 1 : 0
|
|
17108
|
+
);
|
|
17109
|
+
}
|
|
17110
|
+
function rowToInvocation(row) {
|
|
17111
|
+
return {
|
|
17112
|
+
id: row.id,
|
|
17113
|
+
timestamp: row.timestamp,
|
|
17114
|
+
tool_name: row.tool_name,
|
|
17115
|
+
session_id: row.session_id,
|
|
17116
|
+
note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
|
|
17117
|
+
duration_ms: row.duration_ms,
|
|
17118
|
+
success: row.success === 1
|
|
17119
|
+
};
|
|
17120
|
+
}
|
|
17121
|
+
function getToolUsageSummary(stateDb2, daysBack = 30) {
|
|
17122
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
17123
|
+
const rows = stateDb2.db.prepare(`
|
|
17124
|
+
SELECT
|
|
17125
|
+
tool_name,
|
|
17126
|
+
COUNT(*) as invocation_count,
|
|
17127
|
+
AVG(duration_ms) as avg_duration_ms,
|
|
17128
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as success_rate,
|
|
17129
|
+
MAX(timestamp) as last_used
|
|
17130
|
+
FROM tool_invocations
|
|
17131
|
+
WHERE timestamp >= ?
|
|
17132
|
+
GROUP BY tool_name
|
|
17133
|
+
ORDER BY invocation_count DESC
|
|
17134
|
+
`).all(cutoff);
|
|
17135
|
+
return rows.map((r) => ({
|
|
17136
|
+
tool_name: r.tool_name,
|
|
17137
|
+
invocation_count: r.invocation_count,
|
|
17138
|
+
avg_duration_ms: Math.round(r.avg_duration_ms ?? 0),
|
|
17139
|
+
success_rate: Math.round(r.success_rate * 1e3) / 1e3,
|
|
17140
|
+
last_used: r.last_used
|
|
17141
|
+
}));
|
|
17142
|
+
}
|
|
17143
|
+
function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
17144
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
17145
|
+
const rows = stateDb2.db.prepare(`
|
|
17146
|
+
SELECT note_paths, tool_name, timestamp
|
|
17147
|
+
FROM tool_invocations
|
|
17148
|
+
WHERE timestamp >= ? AND note_paths IS NOT NULL
|
|
17149
|
+
ORDER BY timestamp DESC
|
|
17150
|
+
`).all(cutoff);
|
|
17151
|
+
const noteMap = /* @__PURE__ */ new Map();
|
|
17152
|
+
for (const row of rows) {
|
|
17153
|
+
let paths;
|
|
17154
|
+
try {
|
|
17155
|
+
paths = JSON.parse(row.note_paths);
|
|
17156
|
+
} catch {
|
|
17157
|
+
continue;
|
|
17158
|
+
}
|
|
17159
|
+
for (const p of paths) {
|
|
17160
|
+
const existing = noteMap.get(p);
|
|
17161
|
+
if (existing) {
|
|
17162
|
+
existing.access_count++;
|
|
17163
|
+
existing.last_accessed = Math.max(existing.last_accessed, row.timestamp);
|
|
17164
|
+
existing.tools.add(row.tool_name);
|
|
17165
|
+
} else {
|
|
17166
|
+
noteMap.set(p, {
|
|
17167
|
+
access_count: 1,
|
|
17168
|
+
last_accessed: row.timestamp,
|
|
17169
|
+
tools: /* @__PURE__ */ new Set([row.tool_name])
|
|
17170
|
+
});
|
|
17171
|
+
}
|
|
17172
|
+
}
|
|
17173
|
+
}
|
|
17174
|
+
return Array.from(noteMap.entries()).map(([path33, stats]) => ({
|
|
17175
|
+
path: path33,
|
|
17176
|
+
access_count: stats.access_count,
|
|
17177
|
+
last_accessed: stats.last_accessed,
|
|
17178
|
+
tools_used: Array.from(stats.tools)
|
|
17179
|
+
})).sort((a, b) => b.access_count - a.access_count);
|
|
17180
|
+
}
|
|
17181
|
+
function getSessionHistory(stateDb2, sessionId) {
|
|
17182
|
+
if (sessionId) {
|
|
17183
|
+
const rows2 = stateDb2.db.prepare(`
|
|
17184
|
+
SELECT * FROM tool_invocations
|
|
17185
|
+
WHERE session_id = ?
|
|
17186
|
+
ORDER BY timestamp
|
|
17187
|
+
`).all(sessionId);
|
|
17188
|
+
if (rows2.length === 0) return [];
|
|
17189
|
+
const tools = /* @__PURE__ */ new Set();
|
|
17190
|
+
const notes = /* @__PURE__ */ new Set();
|
|
17191
|
+
for (const row of rows2) {
|
|
17192
|
+
tools.add(row.tool_name);
|
|
17193
|
+
if (row.note_paths) {
|
|
17194
|
+
try {
|
|
17195
|
+
for (const p of JSON.parse(row.note_paths)) {
|
|
17196
|
+
notes.add(p);
|
|
17197
|
+
}
|
|
17198
|
+
} catch {
|
|
17199
|
+
}
|
|
17200
|
+
}
|
|
17201
|
+
}
|
|
17202
|
+
return [{
|
|
17203
|
+
session_id: sessionId,
|
|
17204
|
+
started_at: rows2[0].timestamp,
|
|
17205
|
+
last_activity: rows2[rows2.length - 1].timestamp,
|
|
17206
|
+
tool_count: rows2.length,
|
|
17207
|
+
unique_tools: Array.from(tools),
|
|
17208
|
+
notes_accessed: Array.from(notes)
|
|
17209
|
+
}];
|
|
17210
|
+
}
|
|
17211
|
+
const rows = stateDb2.db.prepare(`
|
|
17212
|
+
SELECT
|
|
17213
|
+
session_id,
|
|
17214
|
+
MIN(timestamp) as started_at,
|
|
17215
|
+
MAX(timestamp) as last_activity,
|
|
17216
|
+
COUNT(*) as tool_count
|
|
17217
|
+
FROM tool_invocations
|
|
17218
|
+
WHERE session_id IS NOT NULL
|
|
17219
|
+
GROUP BY session_id
|
|
17220
|
+
ORDER BY last_activity DESC
|
|
17221
|
+
LIMIT 20
|
|
17222
|
+
`).all();
|
|
17223
|
+
return rows.map((r) => ({
|
|
17224
|
+
session_id: r.session_id,
|
|
17225
|
+
started_at: r.started_at,
|
|
17226
|
+
last_activity: r.last_activity,
|
|
17227
|
+
tool_count: r.tool_count,
|
|
17228
|
+
unique_tools: [],
|
|
17229
|
+
notes_accessed: []
|
|
17230
|
+
}));
|
|
17231
|
+
}
|
|
17232
|
+
function getRecentInvocations(stateDb2, limit = 20) {
|
|
17233
|
+
const rows = stateDb2.db.prepare(
|
|
17234
|
+
"SELECT * FROM tool_invocations ORDER BY timestamp DESC LIMIT ?"
|
|
17235
|
+
).all(limit);
|
|
17236
|
+
return rows.map(rowToInvocation);
|
|
17237
|
+
}
|
|
17238
|
+
function purgeOldInvocations(stateDb2, retentionDays = 90) {
|
|
17239
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
17240
|
+
const result = stateDb2.db.prepare(
|
|
17241
|
+
"DELETE FROM tool_invocations WHERE timestamp < ?"
|
|
17242
|
+
).run(cutoff);
|
|
17243
|
+
return result.changes;
|
|
17244
|
+
}
|
|
17245
|
+
|
|
17246
|
+
// src/tools/read/brief.ts
|
|
17247
|
+
function estimateTokens2(value) {
|
|
17248
|
+
const str = JSON.stringify(value);
|
|
17249
|
+
return Math.ceil(str.length / 4);
|
|
17250
|
+
}
|
|
17251
|
+
function buildSessionSection(stateDb2, limit) {
|
|
17252
|
+
const summaries = getRecentSessionSummaries(stateDb2, limit);
|
|
17253
|
+
if (summaries.length > 0) {
|
|
17254
|
+
const content2 = summaries.map((s) => ({
|
|
17255
|
+
session_id: s.session_id,
|
|
17256
|
+
summary: s.summary,
|
|
17257
|
+
topics: s.topics_json ? JSON.parse(s.topics_json) : [],
|
|
17258
|
+
ended_at: s.ended_at,
|
|
17259
|
+
tool_count: s.tool_count
|
|
17260
|
+
}));
|
|
17261
|
+
return {
|
|
17262
|
+
name: "recent_sessions",
|
|
17263
|
+
priority: 1,
|
|
17264
|
+
content: content2,
|
|
17265
|
+
estimated_tokens: estimateTokens2(content2)
|
|
17266
|
+
};
|
|
17267
|
+
}
|
|
17268
|
+
const sessions = getSessionHistory(stateDb2);
|
|
17269
|
+
const recentSessions = sessions.slice(0, limit);
|
|
17270
|
+
if (recentSessions.length === 0) {
|
|
17271
|
+
return { name: "recent_sessions", priority: 1, content: [], estimated_tokens: 0 };
|
|
17272
|
+
}
|
|
17273
|
+
const content = recentSessions.map((s) => ({
|
|
17274
|
+
session_id: s.session_id,
|
|
17275
|
+
started_at: s.started_at,
|
|
17276
|
+
last_activity: s.last_activity,
|
|
17277
|
+
tool_count: s.tool_count,
|
|
17278
|
+
tools_used: s.unique_tools
|
|
17279
|
+
}));
|
|
17280
|
+
return {
|
|
17281
|
+
name: "recent_sessions",
|
|
17282
|
+
priority: 1,
|
|
17283
|
+
content,
|
|
17284
|
+
estimated_tokens: estimateTokens2(content)
|
|
17285
|
+
};
|
|
17286
|
+
}
|
|
17287
|
+
function buildActiveEntitiesSection(stateDb2, limit) {
|
|
17288
|
+
const rows = stateDb2.db.prepare(`
|
|
17289
|
+
SELECT r.entity_name_lower, r.last_mentioned_at, r.mention_count,
|
|
17290
|
+
e.name, e.category, e.description
|
|
17291
|
+
FROM recency r
|
|
17292
|
+
LEFT JOIN entities e ON e.name_lower = r.entity_name_lower
|
|
17293
|
+
ORDER BY r.last_mentioned_at DESC
|
|
17294
|
+
LIMIT ?
|
|
17295
|
+
`).all(limit);
|
|
17296
|
+
const content = rows.map((r) => ({
|
|
17297
|
+
name: r.name || r.entity_name_lower,
|
|
17298
|
+
category: r.category,
|
|
17299
|
+
description: r.description,
|
|
17300
|
+
last_mentioned: r.last_mentioned_at,
|
|
17301
|
+
mentions: r.mention_count
|
|
17302
|
+
}));
|
|
17303
|
+
return {
|
|
17304
|
+
name: "active_entities",
|
|
17305
|
+
priority: 2,
|
|
17306
|
+
content,
|
|
17307
|
+
estimated_tokens: estimateTokens2(content)
|
|
17308
|
+
};
|
|
17309
|
+
}
|
|
17310
|
+
function buildActiveMemoriesSection(stateDb2, limit) {
|
|
17311
|
+
const memories = listMemories(stateDb2, { limit });
|
|
17312
|
+
const content = memories.map((m) => ({
|
|
17313
|
+
key: m.key,
|
|
17314
|
+
value: m.value,
|
|
17315
|
+
type: m.memory_type,
|
|
17316
|
+
entity: m.entity,
|
|
17317
|
+
confidence: m.confidence,
|
|
17318
|
+
updated_at: m.updated_at
|
|
17319
|
+
}));
|
|
17320
|
+
return {
|
|
17321
|
+
name: "active_memories",
|
|
17322
|
+
priority: 3,
|
|
17323
|
+
content,
|
|
17324
|
+
estimated_tokens: estimateTokens2(content)
|
|
17325
|
+
};
|
|
17326
|
+
}
|
|
17327
|
+
function buildCorrectionsSection(stateDb2, limit) {
|
|
17328
|
+
const corrections = listCorrections(stateDb2, "pending", void 0, limit);
|
|
17329
|
+
const content = corrections.map((c) => ({
|
|
17330
|
+
id: c.id,
|
|
17331
|
+
type: c.correction_type,
|
|
17332
|
+
description: c.description,
|
|
17333
|
+
entity: c.entity,
|
|
17334
|
+
created_at: c.created_at
|
|
17335
|
+
}));
|
|
17336
|
+
return {
|
|
17337
|
+
name: "pending_corrections",
|
|
17338
|
+
priority: 4,
|
|
17339
|
+
content,
|
|
17340
|
+
estimated_tokens: estimateTokens2(content)
|
|
17341
|
+
};
|
|
17342
|
+
}
|
|
17343
|
+
function buildVaultPulseSection(stateDb2) {
|
|
17344
|
+
const now = Date.now();
|
|
17345
|
+
const day = 864e5;
|
|
17346
|
+
const recentToolCount = stateDb2.db.prepare(
|
|
17347
|
+
"SELECT COUNT(*) as cnt FROM tool_invocations WHERE timestamp > ?"
|
|
17348
|
+
).get(now - day).cnt;
|
|
17349
|
+
const entityCount = stateDb2.db.prepare(
|
|
17350
|
+
"SELECT COUNT(*) as cnt FROM entities"
|
|
17351
|
+
).get().cnt;
|
|
17352
|
+
const memoryCount = stateDb2.db.prepare(
|
|
17353
|
+
"SELECT COUNT(*) as cnt FROM memories WHERE superseded_by IS NULL"
|
|
17354
|
+
).get().cnt;
|
|
17355
|
+
let noteCount = 0;
|
|
17356
|
+
try {
|
|
17357
|
+
noteCount = stateDb2.db.prepare(
|
|
17358
|
+
"SELECT COUNT(*) as cnt FROM notes_fts"
|
|
17359
|
+
).get().cnt;
|
|
17360
|
+
} catch {
|
|
17361
|
+
}
|
|
17362
|
+
const contradictions = findContradictions2(stateDb2);
|
|
17363
|
+
const content = {
|
|
17364
|
+
notes: noteCount,
|
|
17365
|
+
entities: entityCount,
|
|
17366
|
+
memories: memoryCount,
|
|
17367
|
+
tool_calls_24h: recentToolCount,
|
|
17368
|
+
contradictions: contradictions.length
|
|
17369
|
+
};
|
|
17370
|
+
return {
|
|
17371
|
+
name: "vault_pulse",
|
|
17372
|
+
priority: 5,
|
|
17373
|
+
content,
|
|
17374
|
+
estimated_tokens: estimateTokens2(content)
|
|
17375
|
+
};
|
|
17376
|
+
}
|
|
17377
|
+
function registerBriefTools(server2, getStateDb) {
|
|
17378
|
+
server2.tool(
|
|
17379
|
+
"brief",
|
|
17380
|
+
"Get a startup context briefing: recent sessions, active entities, memories, pending corrections, and vault stats. Call at conversation start.",
|
|
17381
|
+
{
|
|
17382
|
+
max_tokens: z25.number().optional().describe("Token budget (lower-priority sections truncated first)"),
|
|
17383
|
+
focus: z25.string().optional().describe("Focus entity or topic (filters content)"),
|
|
17384
|
+
sections: z25.array(z25.enum(["recent_sessions", "active_entities", "active_memories", "pending_corrections", "vault_pulse"])).optional().describe("Which sections to include (default: all)")
|
|
17385
|
+
},
|
|
17386
|
+
async (args) => {
|
|
17387
|
+
const stateDb2 = getStateDb();
|
|
17388
|
+
if (!stateDb2) {
|
|
17389
|
+
return {
|
|
17390
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
17391
|
+
isError: true
|
|
17392
|
+
};
|
|
17393
|
+
}
|
|
17394
|
+
const requestedSections = args.sections ? new Set(args.sections) : /* @__PURE__ */ new Set(["recent_sessions", "active_entities", "active_memories", "pending_corrections", "vault_pulse"]);
|
|
17395
|
+
const sections = [];
|
|
17396
|
+
if (requestedSections.has("recent_sessions")) {
|
|
17397
|
+
sections.push(buildSessionSection(stateDb2, 5));
|
|
17398
|
+
}
|
|
17399
|
+
if (requestedSections.has("active_entities")) {
|
|
17400
|
+
sections.push(buildActiveEntitiesSection(stateDb2, 10));
|
|
17401
|
+
}
|
|
17402
|
+
if (requestedSections.has("active_memories")) {
|
|
17403
|
+
sections.push(buildActiveMemoriesSection(stateDb2, 20));
|
|
17404
|
+
}
|
|
17405
|
+
if (requestedSections.has("pending_corrections")) {
|
|
17406
|
+
sections.push(buildCorrectionsSection(stateDb2, 10));
|
|
17407
|
+
}
|
|
17408
|
+
if (requestedSections.has("vault_pulse")) {
|
|
17409
|
+
sections.push(buildVaultPulseSection(stateDb2));
|
|
17410
|
+
}
|
|
17411
|
+
if (args.max_tokens) {
|
|
17412
|
+
let totalTokens2 = 0;
|
|
17413
|
+
sections.sort((a, b) => a.priority - b.priority);
|
|
17414
|
+
for (const section of sections) {
|
|
17415
|
+
totalTokens2 += section.estimated_tokens;
|
|
17416
|
+
if (totalTokens2 > args.max_tokens) {
|
|
17417
|
+
if (Array.isArray(section.content)) {
|
|
17418
|
+
const remaining = Math.max(0, args.max_tokens - (totalTokens2 - section.estimated_tokens));
|
|
17419
|
+
const itemTokens = section.estimated_tokens / Math.max(1, section.content.length);
|
|
17420
|
+
const keepCount = Math.max(1, Math.floor(remaining / itemTokens));
|
|
17421
|
+
section.content = section.content.slice(0, keepCount);
|
|
17422
|
+
section.estimated_tokens = estimateTokens2(section.content);
|
|
17423
|
+
}
|
|
17424
|
+
}
|
|
17425
|
+
}
|
|
17426
|
+
}
|
|
17427
|
+
const response = {};
|
|
17428
|
+
let totalTokens = 0;
|
|
17429
|
+
for (const section of sections) {
|
|
17430
|
+
response[section.name] = section.content;
|
|
17431
|
+
totalTokens += section.estimated_tokens;
|
|
17432
|
+
}
|
|
17433
|
+
response._meta = { total_estimated_tokens: totalTokens };
|
|
17434
|
+
return {
|
|
17435
|
+
content: [{
|
|
17436
|
+
type: "text",
|
|
17437
|
+
text: JSON.stringify(response, null, 2)
|
|
17438
|
+
}]
|
|
17439
|
+
};
|
|
17440
|
+
}
|
|
17441
|
+
);
|
|
17442
|
+
}
|
|
17443
|
+
|
|
17444
|
+
// src/tools/write/config.ts
|
|
17445
|
+
import { z as z26 } from "zod";
|
|
17446
|
+
import { saveFlywheelConfigToDb as saveFlywheelConfigToDb2 } from "@velvetmonkey/vault-core";
|
|
17447
|
+
function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
17448
|
+
server2.registerTool(
|
|
17449
|
+
"flywheel_config",
|
|
17450
|
+
{
|
|
17451
|
+
title: "Flywheel Config",
|
|
17452
|
+
description: 'Read or update Flywheel configuration.\n- "get": Returns the current FlywheelConfig\n- "set": Updates a single config key and returns the updated config\n\nExample: flywheel_config({ mode: "get" })\nExample: flywheel_config({ mode: "set", key: "exclude_analysis_tags", value: ["habit", "daily"] })',
|
|
17453
|
+
inputSchema: {
|
|
17454
|
+
mode: z26.enum(["get", "set"]).describe("Operation mode"),
|
|
17455
|
+
key: z26.string().optional().describe("Config key to update (required for set mode)"),
|
|
17456
|
+
value: z26.unknown().optional().describe("New value for the key (required for set mode)")
|
|
17457
|
+
}
|
|
17458
|
+
},
|
|
17459
|
+
async ({ mode, key, value }) => {
|
|
17460
|
+
switch (mode) {
|
|
17461
|
+
case "get": {
|
|
17462
|
+
const config = getConfig();
|
|
17463
|
+
return {
|
|
17464
|
+
content: [{ type: "text", text: JSON.stringify(config, null, 2) }]
|
|
17465
|
+
};
|
|
17466
|
+
}
|
|
17467
|
+
case "set": {
|
|
17468
|
+
if (!key) {
|
|
17469
|
+
return {
|
|
17470
|
+
content: [{ type: "text", text: JSON.stringify({ error: "key is required for set mode" }) }]
|
|
17471
|
+
};
|
|
17472
|
+
}
|
|
17473
|
+
const stateDb2 = getStateDb();
|
|
17474
|
+
if (!stateDb2) {
|
|
17475
|
+
return {
|
|
17476
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
17477
|
+
};
|
|
17478
|
+
}
|
|
17479
|
+
const current = getConfig();
|
|
17480
|
+
const updated = { ...current, [key]: value };
|
|
17481
|
+
saveFlywheelConfigToDb2(stateDb2, updated);
|
|
17482
|
+
const reloaded = loadConfig(stateDb2);
|
|
17483
|
+
setConfig(reloaded);
|
|
17484
|
+
return {
|
|
17485
|
+
content: [{ type: "text", text: JSON.stringify(reloaded, null, 2) }]
|
|
17486
|
+
};
|
|
17487
|
+
}
|
|
17488
|
+
}
|
|
17489
|
+
}
|
|
17490
|
+
);
|
|
17491
|
+
}
|
|
17492
|
+
|
|
17493
|
+
// src/tools/write/enrich.ts
|
|
17494
|
+
import { z as z27 } from "zod";
|
|
17495
|
+
import * as fs29 from "fs/promises";
|
|
17496
|
+
import * as path30 from "path";
|
|
17497
|
+
function hasSkipWikilinks(content) {
|
|
17498
|
+
if (!content.startsWith("---")) return false;
|
|
17499
|
+
const endIndex = content.indexOf("\n---", 3);
|
|
17500
|
+
if (endIndex === -1) return false;
|
|
17501
|
+
const frontmatter = content.substring(4, endIndex);
|
|
17502
|
+
return /^skipWikilinks:\s*true\s*$/m.test(frontmatter);
|
|
17503
|
+
}
|
|
17504
|
+
async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
|
|
17505
|
+
const results = [];
|
|
17506
|
+
try {
|
|
17507
|
+
const entries = await fs29.readdir(dirPath, { withFileTypes: true });
|
|
17508
|
+
for (const entry of entries) {
|
|
17509
|
+
if (entry.name.startsWith(".")) continue;
|
|
17510
|
+
const fullPath = path30.join(dirPath, entry.name);
|
|
17511
|
+
if (entry.isDirectory()) {
|
|
17512
|
+
if (excludeFolders.some((f) => entry.name.toLowerCase() === f.toLowerCase())) continue;
|
|
17513
|
+
const sub = await collectMarkdownFiles(fullPath, basePath, excludeFolders);
|
|
17514
|
+
results.push(...sub);
|
|
17515
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
17516
|
+
results.push(path30.relative(basePath, fullPath));
|
|
17517
|
+
}
|
|
17518
|
+
}
|
|
17519
|
+
} catch {
|
|
17520
|
+
}
|
|
17521
|
+
return results;
|
|
17522
|
+
}
|
|
17523
|
+
var EXCLUDE_FOLDERS = [
|
|
17524
|
+
"daily-notes",
|
|
16418
17525
|
"daily",
|
|
16419
17526
|
"weekly",
|
|
16420
17527
|
"weekly-notes",
|
|
@@ -16439,9 +17546,9 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
16439
17546
|
"vault_init",
|
|
16440
17547
|
"Initialize vault for Flywheel \u2014 scans legacy notes with zero wikilinks and applies entity links. Safe to re-run (idempotent). Use dry_run (default) to preview.",
|
|
16441
17548
|
{
|
|
16442
|
-
dry_run:
|
|
16443
|
-
batch_size:
|
|
16444
|
-
offset:
|
|
17549
|
+
dry_run: z27.boolean().default(true).describe("If true (default), preview what would be linked without modifying files"),
|
|
17550
|
+
batch_size: z27.number().default(50).describe("Maximum notes to process per invocation (default: 50)"),
|
|
17551
|
+
offset: z27.number().default(0).describe("Skip this many eligible notes (for pagination across invocations)")
|
|
16445
17552
|
},
|
|
16446
17553
|
async ({ dry_run, batch_size, offset }) => {
|
|
16447
17554
|
const startTime = Date.now();
|
|
@@ -16536,7 +17643,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
16536
17643
|
}
|
|
16537
17644
|
|
|
16538
17645
|
// src/tools/read/metrics.ts
|
|
16539
|
-
import { z as
|
|
17646
|
+
import { z as z28 } from "zod";
|
|
16540
17647
|
|
|
16541
17648
|
// src/core/shared/metrics.ts
|
|
16542
17649
|
var ALL_METRICS = [
|
|
@@ -16702,10 +17809,10 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16702
17809
|
title: "Vault Growth",
|
|
16703
17810
|
description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago), "index_activity" (rebuild history). Tracks 11 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio, wikilink_accuracy, wikilink_feedback_volume, wikilink_suppressed_count.',
|
|
16704
17811
|
inputSchema: {
|
|
16705
|
-
mode:
|
|
16706
|
-
metric:
|
|
16707
|
-
days_back:
|
|
16708
|
-
limit:
|
|
17812
|
+
mode: z28.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
|
|
17813
|
+
metric: z28.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
|
|
17814
|
+
days_back: z28.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
|
|
17815
|
+
limit: z28.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
|
|
16709
17816
|
}
|
|
16710
17817
|
},
|
|
16711
17818
|
async ({ mode, metric, days_back, limit: eventLimit }) => {
|
|
@@ -16778,159 +17885,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16778
17885
|
}
|
|
16779
17886
|
|
|
16780
17887
|
// src/tools/read/activity.ts
|
|
16781
|
-
import { z as
|
|
16782
|
-
|
|
16783
|
-
// src/core/shared/toolTracking.ts
|
|
16784
|
-
function recordToolInvocation(stateDb2, event) {
|
|
16785
|
-
stateDb2.db.prepare(
|
|
16786
|
-
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
|
|
16787
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
16788
|
-
).run(
|
|
16789
|
-
Date.now(),
|
|
16790
|
-
event.tool_name,
|
|
16791
|
-
event.session_id ?? null,
|
|
16792
|
-
event.note_paths ? JSON.stringify(event.note_paths) : null,
|
|
16793
|
-
event.duration_ms ?? null,
|
|
16794
|
-
event.success !== false ? 1 : 0
|
|
16795
|
-
);
|
|
16796
|
-
}
|
|
16797
|
-
function rowToInvocation(row) {
|
|
16798
|
-
return {
|
|
16799
|
-
id: row.id,
|
|
16800
|
-
timestamp: row.timestamp,
|
|
16801
|
-
tool_name: row.tool_name,
|
|
16802
|
-
session_id: row.session_id,
|
|
16803
|
-
note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
|
|
16804
|
-
duration_ms: row.duration_ms,
|
|
16805
|
-
success: row.success === 1
|
|
16806
|
-
};
|
|
16807
|
-
}
|
|
16808
|
-
function getToolUsageSummary(stateDb2, daysBack = 30) {
|
|
16809
|
-
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
16810
|
-
const rows = stateDb2.db.prepare(`
|
|
16811
|
-
SELECT
|
|
16812
|
-
tool_name,
|
|
16813
|
-
COUNT(*) as invocation_count,
|
|
16814
|
-
AVG(duration_ms) as avg_duration_ms,
|
|
16815
|
-
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as success_rate,
|
|
16816
|
-
MAX(timestamp) as last_used
|
|
16817
|
-
FROM tool_invocations
|
|
16818
|
-
WHERE timestamp >= ?
|
|
16819
|
-
GROUP BY tool_name
|
|
16820
|
-
ORDER BY invocation_count DESC
|
|
16821
|
-
`).all(cutoff);
|
|
16822
|
-
return rows.map((r) => ({
|
|
16823
|
-
tool_name: r.tool_name,
|
|
16824
|
-
invocation_count: r.invocation_count,
|
|
16825
|
-
avg_duration_ms: Math.round(r.avg_duration_ms ?? 0),
|
|
16826
|
-
success_rate: Math.round(r.success_rate * 1e3) / 1e3,
|
|
16827
|
-
last_used: r.last_used
|
|
16828
|
-
}));
|
|
16829
|
-
}
|
|
16830
|
-
function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
16831
|
-
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
16832
|
-
const rows = stateDb2.db.prepare(`
|
|
16833
|
-
SELECT note_paths, tool_name, timestamp
|
|
16834
|
-
FROM tool_invocations
|
|
16835
|
-
WHERE timestamp >= ? AND note_paths IS NOT NULL
|
|
16836
|
-
ORDER BY timestamp DESC
|
|
16837
|
-
`).all(cutoff);
|
|
16838
|
-
const noteMap = /* @__PURE__ */ new Map();
|
|
16839
|
-
for (const row of rows) {
|
|
16840
|
-
let paths;
|
|
16841
|
-
try {
|
|
16842
|
-
paths = JSON.parse(row.note_paths);
|
|
16843
|
-
} catch {
|
|
16844
|
-
continue;
|
|
16845
|
-
}
|
|
16846
|
-
for (const p of paths) {
|
|
16847
|
-
const existing = noteMap.get(p);
|
|
16848
|
-
if (existing) {
|
|
16849
|
-
existing.access_count++;
|
|
16850
|
-
existing.last_accessed = Math.max(existing.last_accessed, row.timestamp);
|
|
16851
|
-
existing.tools.add(row.tool_name);
|
|
16852
|
-
} else {
|
|
16853
|
-
noteMap.set(p, {
|
|
16854
|
-
access_count: 1,
|
|
16855
|
-
last_accessed: row.timestamp,
|
|
16856
|
-
tools: /* @__PURE__ */ new Set([row.tool_name])
|
|
16857
|
-
});
|
|
16858
|
-
}
|
|
16859
|
-
}
|
|
16860
|
-
}
|
|
16861
|
-
return Array.from(noteMap.entries()).map(([path33, stats]) => ({
|
|
16862
|
-
path: path33,
|
|
16863
|
-
access_count: stats.access_count,
|
|
16864
|
-
last_accessed: stats.last_accessed,
|
|
16865
|
-
tools_used: Array.from(stats.tools)
|
|
16866
|
-
})).sort((a, b) => b.access_count - a.access_count);
|
|
16867
|
-
}
|
|
16868
|
-
function getSessionHistory(stateDb2, sessionId) {
|
|
16869
|
-
if (sessionId) {
|
|
16870
|
-
const rows2 = stateDb2.db.prepare(`
|
|
16871
|
-
SELECT * FROM tool_invocations
|
|
16872
|
-
WHERE session_id = ?
|
|
16873
|
-
ORDER BY timestamp
|
|
16874
|
-
`).all(sessionId);
|
|
16875
|
-
if (rows2.length === 0) return [];
|
|
16876
|
-
const tools = /* @__PURE__ */ new Set();
|
|
16877
|
-
const notes = /* @__PURE__ */ new Set();
|
|
16878
|
-
for (const row of rows2) {
|
|
16879
|
-
tools.add(row.tool_name);
|
|
16880
|
-
if (row.note_paths) {
|
|
16881
|
-
try {
|
|
16882
|
-
for (const p of JSON.parse(row.note_paths)) {
|
|
16883
|
-
notes.add(p);
|
|
16884
|
-
}
|
|
16885
|
-
} catch {
|
|
16886
|
-
}
|
|
16887
|
-
}
|
|
16888
|
-
}
|
|
16889
|
-
return [{
|
|
16890
|
-
session_id: sessionId,
|
|
16891
|
-
started_at: rows2[0].timestamp,
|
|
16892
|
-
last_activity: rows2[rows2.length - 1].timestamp,
|
|
16893
|
-
tool_count: rows2.length,
|
|
16894
|
-
unique_tools: Array.from(tools),
|
|
16895
|
-
notes_accessed: Array.from(notes)
|
|
16896
|
-
}];
|
|
16897
|
-
}
|
|
16898
|
-
const rows = stateDb2.db.prepare(`
|
|
16899
|
-
SELECT
|
|
16900
|
-
session_id,
|
|
16901
|
-
MIN(timestamp) as started_at,
|
|
16902
|
-
MAX(timestamp) as last_activity,
|
|
16903
|
-
COUNT(*) as tool_count
|
|
16904
|
-
FROM tool_invocations
|
|
16905
|
-
WHERE session_id IS NOT NULL
|
|
16906
|
-
GROUP BY session_id
|
|
16907
|
-
ORDER BY last_activity DESC
|
|
16908
|
-
LIMIT 20
|
|
16909
|
-
`).all();
|
|
16910
|
-
return rows.map((r) => ({
|
|
16911
|
-
session_id: r.session_id,
|
|
16912
|
-
started_at: r.started_at,
|
|
16913
|
-
last_activity: r.last_activity,
|
|
16914
|
-
tool_count: r.tool_count,
|
|
16915
|
-
unique_tools: [],
|
|
16916
|
-
notes_accessed: []
|
|
16917
|
-
}));
|
|
16918
|
-
}
|
|
16919
|
-
function getRecentInvocations(stateDb2, limit = 20) {
|
|
16920
|
-
const rows = stateDb2.db.prepare(
|
|
16921
|
-
"SELECT * FROM tool_invocations ORDER BY timestamp DESC LIMIT ?"
|
|
16922
|
-
).all(limit);
|
|
16923
|
-
return rows.map(rowToInvocation);
|
|
16924
|
-
}
|
|
16925
|
-
function purgeOldInvocations(stateDb2, retentionDays = 90) {
|
|
16926
|
-
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
16927
|
-
const result = stateDb2.db.prepare(
|
|
16928
|
-
"DELETE FROM tool_invocations WHERE timestamp < ?"
|
|
16929
|
-
).run(cutoff);
|
|
16930
|
-
return result.changes;
|
|
16931
|
-
}
|
|
16932
|
-
|
|
16933
|
-
// src/tools/read/activity.ts
|
|
17888
|
+
import { z as z29 } from "zod";
|
|
16934
17889
|
function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
16935
17890
|
server2.registerTool(
|
|
16936
17891
|
"vault_activity",
|
|
@@ -16938,10 +17893,10 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
16938
17893
|
title: "Vault Activity",
|
|
16939
17894
|
description: 'Track tool usage patterns and session activity. Modes:\n- "session": Current session summary (tools called, notes accessed)\n- "sessions": List of recent sessions\n- "note_access": Notes ranked by query frequency\n- "tool_usage": Tool usage patterns (most-used tools, avg duration)',
|
|
16940
17895
|
inputSchema: {
|
|
16941
|
-
mode:
|
|
16942
|
-
session_id:
|
|
16943
|
-
days_back:
|
|
16944
|
-
limit:
|
|
17896
|
+
mode: z29.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
|
|
17897
|
+
session_id: z29.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
|
|
17898
|
+
days_back: z29.number().optional().describe("Number of days to look back (default: 30)"),
|
|
17899
|
+
limit: z29.number().optional().describe("Maximum results to return (default: 20)")
|
|
16945
17900
|
}
|
|
16946
17901
|
},
|
|
16947
17902
|
async ({ mode, session_id, days_back, limit: resultLimit }) => {
|
|
@@ -17008,7 +17963,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
17008
17963
|
}
|
|
17009
17964
|
|
|
17010
17965
|
// src/tools/read/similarity.ts
|
|
17011
|
-
import { z as
|
|
17966
|
+
import { z as z30 } from "zod";
|
|
17012
17967
|
|
|
17013
17968
|
// src/core/read/similarity.ts
|
|
17014
17969
|
import * as fs30 from "fs";
|
|
@@ -17272,9 +18227,9 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
17272
18227
|
title: "Find Similar Notes",
|
|
17273
18228
|
description: "Find notes similar to a given note using FTS5 keyword matching. When embeddings have been built (via init_semantic), automatically uses hybrid ranking (BM25 + embedding similarity via Reciprocal Rank Fusion). Use exclude_linked to filter out notes already connected via wikilinks.",
|
|
17274
18229
|
inputSchema: {
|
|
17275
|
-
path:
|
|
17276
|
-
limit:
|
|
17277
|
-
exclude_linked:
|
|
18230
|
+
path: z30.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
|
|
18231
|
+
limit: z30.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
|
|
18232
|
+
exclude_linked: z30.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
|
|
17278
18233
|
}
|
|
17279
18234
|
},
|
|
17280
18235
|
async ({ path: path33, limit, exclude_linked }) => {
|
|
@@ -17318,7 +18273,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
17318
18273
|
}
|
|
17319
18274
|
|
|
17320
18275
|
// src/tools/read/semantic.ts
|
|
17321
|
-
import { z as
|
|
18276
|
+
import { z as z31 } from "zod";
|
|
17322
18277
|
import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
|
|
17323
18278
|
function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
17324
18279
|
server2.registerTool(
|
|
@@ -17327,7 +18282,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
17327
18282
|
title: "Initialize Semantic Search",
|
|
17328
18283
|
description: "Download the embedding model and build semantic search index for this vault. After running, search and find_similar automatically use hybrid ranking (BM25 + semantic). Run once per vault \u2014 subsequent calls skip already-embedded notes unless force=true.",
|
|
17329
18284
|
inputSchema: {
|
|
17330
|
-
force:
|
|
18285
|
+
force: z31.boolean().optional().describe(
|
|
17331
18286
|
"Rebuild all embeddings even if they already exist (default: false)"
|
|
17332
18287
|
)
|
|
17333
18288
|
}
|
|
@@ -17407,7 +18362,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
17407
18362
|
|
|
17408
18363
|
// src/tools/read/merges.ts
|
|
17409
18364
|
init_levenshtein();
|
|
17410
|
-
import { z as
|
|
18365
|
+
import { z as z32 } from "zod";
|
|
17411
18366
|
import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
|
|
17412
18367
|
function normalizeName(name) {
|
|
17413
18368
|
return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
|
|
@@ -17417,7 +18372,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
17417
18372
|
"suggest_entity_merges",
|
|
17418
18373
|
"Find potential duplicate entities that could be merged based on name similarity",
|
|
17419
18374
|
{
|
|
17420
|
-
limit:
|
|
18375
|
+
limit: z32.number().optional().default(50).describe("Maximum number of suggestions to return")
|
|
17421
18376
|
},
|
|
17422
18377
|
async ({ limit }) => {
|
|
17423
18378
|
const stateDb2 = getStateDb();
|
|
@@ -17519,11 +18474,11 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
17519
18474
|
"dismiss_merge_suggestion",
|
|
17520
18475
|
"Permanently dismiss a merge suggestion so it never reappears",
|
|
17521
18476
|
{
|
|
17522
|
-
source_path:
|
|
17523
|
-
target_path:
|
|
17524
|
-
source_name:
|
|
17525
|
-
target_name:
|
|
17526
|
-
reason:
|
|
18477
|
+
source_path: z32.string().describe("Path of the source entity"),
|
|
18478
|
+
target_path: z32.string().describe("Path of the target entity"),
|
|
18479
|
+
source_name: z32.string().describe("Name of the source entity"),
|
|
18480
|
+
target_name: z32.string().describe("Name of the target entity"),
|
|
18481
|
+
reason: z32.string().describe("Original suggestion reason")
|
|
17527
18482
|
},
|
|
17528
18483
|
async ({ source_path, target_path, source_name, target_name, reason }) => {
|
|
17529
18484
|
const stateDb2 = getStateDb();
|
|
@@ -17684,8 +18639,10 @@ var PRESETS = {
|
|
|
17684
18639
|
"frontmatter",
|
|
17685
18640
|
"notes",
|
|
17686
18641
|
"git",
|
|
17687
|
-
"policy"
|
|
18642
|
+
"policy",
|
|
18643
|
+
"memory"
|
|
17688
18644
|
],
|
|
18645
|
+
agent: ["search", "structure", "append", "frontmatter", "notes", "memory"],
|
|
17689
18646
|
// Composable bundles
|
|
17690
18647
|
graph: ["backlinks", "orphans", "hubs", "paths"],
|
|
17691
18648
|
analysis: ["schema", "wikilinks"],
|
|
@@ -17708,7 +18665,8 @@ var ALL_CATEGORIES = [
|
|
|
17708
18665
|
"frontmatter",
|
|
17709
18666
|
"notes",
|
|
17710
18667
|
"git",
|
|
17711
|
-
"policy"
|
|
18668
|
+
"policy",
|
|
18669
|
+
"memory"
|
|
17712
18670
|
];
|
|
17713
18671
|
var DEFAULT_PRESET = "full";
|
|
17714
18672
|
function parseEnabledCategories() {
|
|
@@ -17812,7 +18770,11 @@ var TOOL_CATEGORY = {
|
|
|
17812
18770
|
suggest_entity_merges: "health",
|
|
17813
18771
|
dismiss_merge_suggestion: "health",
|
|
17814
18772
|
// notes (entity merge)
|
|
17815
|
-
merge_entities: "notes"
|
|
18773
|
+
merge_entities: "notes",
|
|
18774
|
+
// memory (agent working memory)
|
|
18775
|
+
memory: "memory",
|
|
18776
|
+
recall: "memory",
|
|
18777
|
+
brief: "memory"
|
|
17816
18778
|
};
|
|
17817
18779
|
var server = new McpServer({
|
|
17818
18780
|
name: "flywheel-memory",
|
|
@@ -17944,6 +18906,9 @@ registerActivityTools(server, () => stateDb, () => {
|
|
|
17944
18906
|
registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
17945
18907
|
registerSemanticTools(server, () => vaultPath, () => stateDb);
|
|
17946
18908
|
registerMergeTools2(server, () => stateDb);
|
|
18909
|
+
registerMemoryTools(server, () => stateDb);
|
|
18910
|
+
registerRecallTools(server, () => stateDb);
|
|
18911
|
+
registerBriefTools(server, () => stateDb);
|
|
17947
18912
|
registerVaultResources(server, () => vaultIndex ?? null);
|
|
17948
18913
|
serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
17949
18914
|
async function main() {
|
|
@@ -18625,14 +19590,22 @@ async function runPostIndexWork(index) {
|
|
|
18625
19590
|
}
|
|
18626
19591
|
}
|
|
18627
19592
|
}
|
|
19593
|
+
const newDeadLinks = [];
|
|
19594
|
+
for (const diff of linkDiffs) {
|
|
19595
|
+
const newDead = diff.added.filter((target) => !vaultIndex.entities.has(target.toLowerCase()));
|
|
19596
|
+
if (newDead.length > 0) {
|
|
19597
|
+
newDeadLinks.push({ file: diff.file, targets: newDead });
|
|
19598
|
+
}
|
|
19599
|
+
}
|
|
18628
19600
|
tracker.end({
|
|
18629
19601
|
total_resolved: totalResolved,
|
|
18630
19602
|
total_dead: totalDead,
|
|
18631
19603
|
links: forwardLinkResults,
|
|
18632
19604
|
link_diffs: linkDiffs,
|
|
18633
|
-
survived: survivedLinks
|
|
19605
|
+
survived: survivedLinks,
|
|
19606
|
+
new_dead_links: newDeadLinks
|
|
18634
19607
|
});
|
|
18635
|
-
serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead`);
|
|
19608
|
+
serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead${newDeadLinks.length > 0 ? `, ${newDeadLinks.reduce((s, d) => s + d.targets.length, 0)} new dead` : ""}`);
|
|
18636
19609
|
tracker.start("wikilink_check", { files: filteredEvents.length });
|
|
18637
19610
|
const trackedLinks = [];
|
|
18638
19611
|
if (stateDb) {
|
|
@@ -18696,6 +19669,7 @@ async function runPostIndexWork(index) {
|
|
|
18696
19669
|
tracker.end({ tracked: trackedLinks, mentions: mentionResults });
|
|
18697
19670
|
serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files, ${mentionResults.reduce((s, m) => s + m.entities.length, 0)} unwikified mentions`);
|
|
18698
19671
|
tracker.start("implicit_feedback", { files: filteredEvents.length });
|
|
19672
|
+
const preSuppressed = stateDb ? new Set(getAllSuppressionPenalties(stateDb).keys()) : /* @__PURE__ */ new Set();
|
|
18699
19673
|
const feedbackResults = [];
|
|
18700
19674
|
if (stateDb) {
|
|
18701
19675
|
for (const event of filteredEvents) {
|
|
@@ -18740,10 +19714,84 @@ async function runPostIndexWork(index) {
|
|
|
18740
19714
|
}
|
|
18741
19715
|
}
|
|
18742
19716
|
}
|
|
18743
|
-
|
|
19717
|
+
const newlySuppressed = [];
|
|
19718
|
+
if (stateDb) {
|
|
19719
|
+
const postSuppressed = getAllSuppressionPenalties(stateDb);
|
|
19720
|
+
for (const entity of postSuppressed.keys()) {
|
|
19721
|
+
if (!preSuppressed.has(entity)) {
|
|
19722
|
+
newlySuppressed.push(entity);
|
|
19723
|
+
}
|
|
19724
|
+
}
|
|
19725
|
+
}
|
|
19726
|
+
tracker.end({ removals: feedbackResults, additions: additionResults, newly_suppressed: newlySuppressed });
|
|
18744
19727
|
if (feedbackResults.length > 0 || additionResults.length > 0) {
|
|
18745
19728
|
serverLog("watcher", `Implicit feedback: ${feedbackResults.length} removals, ${additionResults.length} manual additions detected`);
|
|
18746
19729
|
}
|
|
19730
|
+
if (newlySuppressed.length > 0) {
|
|
19731
|
+
serverLog("watcher", `Suppression: ${newlySuppressed.length} entities newly suppressed: ${newlySuppressed.join(", ")}`);
|
|
19732
|
+
}
|
|
19733
|
+
tracker.start("prospect_scan", { files: filteredEvents.length });
|
|
19734
|
+
const prospectResults = [];
|
|
19735
|
+
for (const event of filteredEvents) {
|
|
19736
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
19737
|
+
try {
|
|
19738
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
19739
|
+
const zones = getProtectedZones2(content);
|
|
19740
|
+
const linkedSet = new Set(
|
|
19741
|
+
(forwardLinkResults.find((r) => r.file === event.path)?.resolved ?? []).concat(forwardLinkResults.find((r) => r.file === event.path)?.dead ?? []).map((n) => n.toLowerCase())
|
|
19742
|
+
);
|
|
19743
|
+
const knownEntitySet = new Set(entitiesAfter.map((e) => e.nameLower));
|
|
19744
|
+
const implicitMatches = detectImplicitEntities3(content);
|
|
19745
|
+
const implicitNames = implicitMatches.filter((imp) => !linkedSet.has(imp.text.toLowerCase()) && !knownEntitySet.has(imp.text.toLowerCase())).map((imp) => imp.text);
|
|
19746
|
+
const deadLinkMatches = [];
|
|
19747
|
+
for (const [key, links] of vaultIndex.backlinks) {
|
|
19748
|
+
if (links.length < 2 || vaultIndex.entities.has(key) || linkedSet.has(key)) continue;
|
|
19749
|
+
const matches = findEntityMatches2(content, key, true);
|
|
19750
|
+
if (matches.some((m) => !rangeOverlapsProtectedZone(m.start, m.end, zones))) {
|
|
19751
|
+
deadLinkMatches.push(key);
|
|
19752
|
+
}
|
|
19753
|
+
}
|
|
19754
|
+
if (implicitNames.length > 0 || deadLinkMatches.length > 0) {
|
|
19755
|
+
prospectResults.push({ file: event.path, implicit: implicitNames, deadLinkMatches });
|
|
19756
|
+
}
|
|
19757
|
+
} catch {
|
|
19758
|
+
}
|
|
19759
|
+
}
|
|
19760
|
+
tracker.end({ prospects: prospectResults });
|
|
19761
|
+
if (prospectResults.length > 0) {
|
|
19762
|
+
const implicitCount = prospectResults.reduce((s, p) => s + p.implicit.length, 0);
|
|
19763
|
+
const deadCount = prospectResults.reduce((s, p) => s + p.deadLinkMatches.length, 0);
|
|
19764
|
+
serverLog("watcher", `Prospect scan: ${implicitCount} implicit entities, ${deadCount} dead link matches across ${prospectResults.length} files`);
|
|
19765
|
+
}
|
|
19766
|
+
tracker.start("suggestion_scoring", { files: filteredEvents.length });
|
|
19767
|
+
const suggestionResults = [];
|
|
19768
|
+
for (const event of filteredEvents) {
|
|
19769
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
19770
|
+
try {
|
|
19771
|
+
const content = await fs31.readFile(path32.join(vaultPath, event.path), "utf-8");
|
|
19772
|
+
const result = await suggestRelatedLinks(content, {
|
|
19773
|
+
maxSuggestions: 5,
|
|
19774
|
+
strictness: "balanced",
|
|
19775
|
+
notePath: event.path,
|
|
19776
|
+
detail: true
|
|
19777
|
+
});
|
|
19778
|
+
if (result.detailed && result.detailed.length > 0) {
|
|
19779
|
+
suggestionResults.push({
|
|
19780
|
+
file: event.path,
|
|
19781
|
+
top: result.detailed.slice(0, 5).map((s) => ({
|
|
19782
|
+
entity: s.entity,
|
|
19783
|
+
score: s.totalScore,
|
|
19784
|
+
confidence: s.confidence
|
|
19785
|
+
}))
|
|
19786
|
+
});
|
|
19787
|
+
}
|
|
19788
|
+
} catch {
|
|
19789
|
+
}
|
|
19790
|
+
}
|
|
19791
|
+
tracker.end({ scored_files: suggestionResults.length, suggestions: suggestionResults });
|
|
19792
|
+
if (suggestionResults.length > 0) {
|
|
19793
|
+
serverLog("watcher", `Suggestion scoring: ${suggestionResults.length} files scored`);
|
|
19794
|
+
}
|
|
18747
19795
|
tracker.start("tag_scan", { files: filteredEvents.length });
|
|
18748
19796
|
const tagDiffs = [];
|
|
18749
19797
|
if (stateDb) {
|