@velvetmonkey/flywheel-memory 2.0.50 → 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 +1095 -186
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16390,8 +16390,1059 @@ function registerCorrectionTools(server2, getStateDb) {
|
|
|
16390
16390
|
);
|
|
16391
16391
|
}
|
|
16392
16392
|
|
|
16393
|
+
// src/tools/write/memory.ts
|
|
16394
|
+
import { z as z23 } from "zod";
|
|
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] });
|
|
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")
|
|
16668
|
+
},
|
|
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
|
+
}
|
|
16725
|
+
case "get": {
|
|
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
|
+
}
|
|
16738
|
+
return {
|
|
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
|
+
}]
|
|
16756
|
+
};
|
|
16757
|
+
}
|
|
16758
|
+
case "search": {
|
|
16759
|
+
if (!args.query) {
|
|
16760
|
+
return {
|
|
16761
|
+
content: [{ type: "text", text: JSON.stringify({ error: "search requires query" }) }],
|
|
16762
|
+
isError: true
|
|
16763
|
+
};
|
|
16764
|
+
}
|
|
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) {
|
|
16815
|
+
return {
|
|
16816
|
+
content: [{ type: "text", text: JSON.stringify({ error: "forget requires key" }) }],
|
|
16817
|
+
isError: true
|
|
16818
|
+
};
|
|
16819
|
+
}
|
|
16820
|
+
const deleted = forgetMemory(stateDb2, args.key);
|
|
16821
|
+
return {
|
|
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
|
+
}]
|
|
16851
|
+
};
|
|
16852
|
+
}
|
|
16853
|
+
default:
|
|
16854
|
+
return {
|
|
16855
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${args.action}` }) }],
|
|
16856
|
+
isError: true
|
|
16857
|
+
};
|
|
16858
|
+
}
|
|
16859
|
+
}
|
|
16860
|
+
);
|
|
16861
|
+
}
|
|
16862
|
+
|
|
16863
|
+
// src/tools/read/recall.ts
|
|
16864
|
+
import { z as z24 } from "zod";
|
|
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;
|
|
16886
|
+
}
|
|
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;
|
|
16899
|
+
const results = [];
|
|
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
|
+
}
|
|
16928
|
+
}
|
|
16929
|
+
} catch {
|
|
16930
|
+
}
|
|
16931
|
+
}
|
|
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;
|
|
17035
|
+
}
|
|
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
|
+
|
|
16393
17444
|
// src/tools/write/config.ts
|
|
16394
|
-
import { z as
|
|
17445
|
+
import { z as z26 } from "zod";
|
|
16395
17446
|
import { saveFlywheelConfigToDb as saveFlywheelConfigToDb2 } from "@velvetmonkey/vault-core";
|
|
16396
17447
|
function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
16397
17448
|
server2.registerTool(
|
|
@@ -16400,9 +17451,9 @@ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
|
16400
17451
|
title: "Flywheel Config",
|
|
16401
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"] })',
|
|
16402
17453
|
inputSchema: {
|
|
16403
|
-
mode:
|
|
16404
|
-
key:
|
|
16405
|
-
value:
|
|
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)")
|
|
16406
17457
|
}
|
|
16407
17458
|
},
|
|
16408
17459
|
async ({ mode, key, value }) => {
|
|
@@ -16440,7 +17491,7 @@ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
|
16440
17491
|
}
|
|
16441
17492
|
|
|
16442
17493
|
// src/tools/write/enrich.ts
|
|
16443
|
-
import { z as
|
|
17494
|
+
import { z as z27 } from "zod";
|
|
16444
17495
|
import * as fs29 from "fs/promises";
|
|
16445
17496
|
import * as path30 from "path";
|
|
16446
17497
|
function hasSkipWikilinks(content) {
|
|
@@ -16495,9 +17546,9 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
16495
17546
|
"vault_init",
|
|
16496
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.",
|
|
16497
17548
|
{
|
|
16498
|
-
dry_run:
|
|
16499
|
-
batch_size:
|
|
16500
|
-
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)")
|
|
16501
17552
|
},
|
|
16502
17553
|
async ({ dry_run, batch_size, offset }) => {
|
|
16503
17554
|
const startTime = Date.now();
|
|
@@ -16592,7 +17643,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
16592
17643
|
}
|
|
16593
17644
|
|
|
16594
17645
|
// src/tools/read/metrics.ts
|
|
16595
|
-
import { z as
|
|
17646
|
+
import { z as z28 } from "zod";
|
|
16596
17647
|
|
|
16597
17648
|
// src/core/shared/metrics.ts
|
|
16598
17649
|
var ALL_METRICS = [
|
|
@@ -16758,10 +17809,10 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16758
17809
|
title: "Vault Growth",
|
|
16759
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.',
|
|
16760
17811
|
inputSchema: {
|
|
16761
|
-
mode:
|
|
16762
|
-
metric:
|
|
16763
|
-
days_back:
|
|
16764
|
-
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)")
|
|
16765
17816
|
}
|
|
16766
17817
|
},
|
|
16767
17818
|
async ({ mode, metric, days_back, limit: eventLimit }) => {
|
|
@@ -16834,159 +17885,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16834
17885
|
}
|
|
16835
17886
|
|
|
16836
17887
|
// src/tools/read/activity.ts
|
|
16837
|
-
import { z as
|
|
16838
|
-
|
|
16839
|
-
// src/core/shared/toolTracking.ts
|
|
16840
|
-
function recordToolInvocation(stateDb2, event) {
|
|
16841
|
-
stateDb2.db.prepare(
|
|
16842
|
-
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
|
|
16843
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
16844
|
-
).run(
|
|
16845
|
-
Date.now(),
|
|
16846
|
-
event.tool_name,
|
|
16847
|
-
event.session_id ?? null,
|
|
16848
|
-
event.note_paths ? JSON.stringify(event.note_paths) : null,
|
|
16849
|
-
event.duration_ms ?? null,
|
|
16850
|
-
event.success !== false ? 1 : 0
|
|
16851
|
-
);
|
|
16852
|
-
}
|
|
16853
|
-
function rowToInvocation(row) {
|
|
16854
|
-
return {
|
|
16855
|
-
id: row.id,
|
|
16856
|
-
timestamp: row.timestamp,
|
|
16857
|
-
tool_name: row.tool_name,
|
|
16858
|
-
session_id: row.session_id,
|
|
16859
|
-
note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
|
|
16860
|
-
duration_ms: row.duration_ms,
|
|
16861
|
-
success: row.success === 1
|
|
16862
|
-
};
|
|
16863
|
-
}
|
|
16864
|
-
function getToolUsageSummary(stateDb2, daysBack = 30) {
|
|
16865
|
-
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
16866
|
-
const rows = stateDb2.db.prepare(`
|
|
16867
|
-
SELECT
|
|
16868
|
-
tool_name,
|
|
16869
|
-
COUNT(*) as invocation_count,
|
|
16870
|
-
AVG(duration_ms) as avg_duration_ms,
|
|
16871
|
-
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as success_rate,
|
|
16872
|
-
MAX(timestamp) as last_used
|
|
16873
|
-
FROM tool_invocations
|
|
16874
|
-
WHERE timestamp >= ?
|
|
16875
|
-
GROUP BY tool_name
|
|
16876
|
-
ORDER BY invocation_count DESC
|
|
16877
|
-
`).all(cutoff);
|
|
16878
|
-
return rows.map((r) => ({
|
|
16879
|
-
tool_name: r.tool_name,
|
|
16880
|
-
invocation_count: r.invocation_count,
|
|
16881
|
-
avg_duration_ms: Math.round(r.avg_duration_ms ?? 0),
|
|
16882
|
-
success_rate: Math.round(r.success_rate * 1e3) / 1e3,
|
|
16883
|
-
last_used: r.last_used
|
|
16884
|
-
}));
|
|
16885
|
-
}
|
|
16886
|
-
function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
16887
|
-
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
16888
|
-
const rows = stateDb2.db.prepare(`
|
|
16889
|
-
SELECT note_paths, tool_name, timestamp
|
|
16890
|
-
FROM tool_invocations
|
|
16891
|
-
WHERE timestamp >= ? AND note_paths IS NOT NULL
|
|
16892
|
-
ORDER BY timestamp DESC
|
|
16893
|
-
`).all(cutoff);
|
|
16894
|
-
const noteMap = /* @__PURE__ */ new Map();
|
|
16895
|
-
for (const row of rows) {
|
|
16896
|
-
let paths;
|
|
16897
|
-
try {
|
|
16898
|
-
paths = JSON.parse(row.note_paths);
|
|
16899
|
-
} catch {
|
|
16900
|
-
continue;
|
|
16901
|
-
}
|
|
16902
|
-
for (const p of paths) {
|
|
16903
|
-
const existing = noteMap.get(p);
|
|
16904
|
-
if (existing) {
|
|
16905
|
-
existing.access_count++;
|
|
16906
|
-
existing.last_accessed = Math.max(existing.last_accessed, row.timestamp);
|
|
16907
|
-
existing.tools.add(row.tool_name);
|
|
16908
|
-
} else {
|
|
16909
|
-
noteMap.set(p, {
|
|
16910
|
-
access_count: 1,
|
|
16911
|
-
last_accessed: row.timestamp,
|
|
16912
|
-
tools: /* @__PURE__ */ new Set([row.tool_name])
|
|
16913
|
-
});
|
|
16914
|
-
}
|
|
16915
|
-
}
|
|
16916
|
-
}
|
|
16917
|
-
return Array.from(noteMap.entries()).map(([path33, stats]) => ({
|
|
16918
|
-
path: path33,
|
|
16919
|
-
access_count: stats.access_count,
|
|
16920
|
-
last_accessed: stats.last_accessed,
|
|
16921
|
-
tools_used: Array.from(stats.tools)
|
|
16922
|
-
})).sort((a, b) => b.access_count - a.access_count);
|
|
16923
|
-
}
|
|
16924
|
-
function getSessionHistory(stateDb2, sessionId) {
|
|
16925
|
-
if (sessionId) {
|
|
16926
|
-
const rows2 = stateDb2.db.prepare(`
|
|
16927
|
-
SELECT * FROM tool_invocations
|
|
16928
|
-
WHERE session_id = ?
|
|
16929
|
-
ORDER BY timestamp
|
|
16930
|
-
`).all(sessionId);
|
|
16931
|
-
if (rows2.length === 0) return [];
|
|
16932
|
-
const tools = /* @__PURE__ */ new Set();
|
|
16933
|
-
const notes = /* @__PURE__ */ new Set();
|
|
16934
|
-
for (const row of rows2) {
|
|
16935
|
-
tools.add(row.tool_name);
|
|
16936
|
-
if (row.note_paths) {
|
|
16937
|
-
try {
|
|
16938
|
-
for (const p of JSON.parse(row.note_paths)) {
|
|
16939
|
-
notes.add(p);
|
|
16940
|
-
}
|
|
16941
|
-
} catch {
|
|
16942
|
-
}
|
|
16943
|
-
}
|
|
16944
|
-
}
|
|
16945
|
-
return [{
|
|
16946
|
-
session_id: sessionId,
|
|
16947
|
-
started_at: rows2[0].timestamp,
|
|
16948
|
-
last_activity: rows2[rows2.length - 1].timestamp,
|
|
16949
|
-
tool_count: rows2.length,
|
|
16950
|
-
unique_tools: Array.from(tools),
|
|
16951
|
-
notes_accessed: Array.from(notes)
|
|
16952
|
-
}];
|
|
16953
|
-
}
|
|
16954
|
-
const rows = stateDb2.db.prepare(`
|
|
16955
|
-
SELECT
|
|
16956
|
-
session_id,
|
|
16957
|
-
MIN(timestamp) as started_at,
|
|
16958
|
-
MAX(timestamp) as last_activity,
|
|
16959
|
-
COUNT(*) as tool_count
|
|
16960
|
-
FROM tool_invocations
|
|
16961
|
-
WHERE session_id IS NOT NULL
|
|
16962
|
-
GROUP BY session_id
|
|
16963
|
-
ORDER BY last_activity DESC
|
|
16964
|
-
LIMIT 20
|
|
16965
|
-
`).all();
|
|
16966
|
-
return rows.map((r) => ({
|
|
16967
|
-
session_id: r.session_id,
|
|
16968
|
-
started_at: r.started_at,
|
|
16969
|
-
last_activity: r.last_activity,
|
|
16970
|
-
tool_count: r.tool_count,
|
|
16971
|
-
unique_tools: [],
|
|
16972
|
-
notes_accessed: []
|
|
16973
|
-
}));
|
|
16974
|
-
}
|
|
16975
|
-
function getRecentInvocations(stateDb2, limit = 20) {
|
|
16976
|
-
const rows = stateDb2.db.prepare(
|
|
16977
|
-
"SELECT * FROM tool_invocations ORDER BY timestamp DESC LIMIT ?"
|
|
16978
|
-
).all(limit);
|
|
16979
|
-
return rows.map(rowToInvocation);
|
|
16980
|
-
}
|
|
16981
|
-
function purgeOldInvocations(stateDb2, retentionDays = 90) {
|
|
16982
|
-
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
16983
|
-
const result = stateDb2.db.prepare(
|
|
16984
|
-
"DELETE FROM tool_invocations WHERE timestamp < ?"
|
|
16985
|
-
).run(cutoff);
|
|
16986
|
-
return result.changes;
|
|
16987
|
-
}
|
|
16988
|
-
|
|
16989
|
-
// src/tools/read/activity.ts
|
|
17888
|
+
import { z as z29 } from "zod";
|
|
16990
17889
|
function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
16991
17890
|
server2.registerTool(
|
|
16992
17891
|
"vault_activity",
|
|
@@ -16994,10 +17893,10 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
16994
17893
|
title: "Vault Activity",
|
|
16995
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)',
|
|
16996
17895
|
inputSchema: {
|
|
16997
|
-
mode:
|
|
16998
|
-
session_id:
|
|
16999
|
-
days_back:
|
|
17000
|
-
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)")
|
|
17001
17900
|
}
|
|
17002
17901
|
},
|
|
17003
17902
|
async ({ mode, session_id, days_back, limit: resultLimit }) => {
|
|
@@ -17064,7 +17963,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
17064
17963
|
}
|
|
17065
17964
|
|
|
17066
17965
|
// src/tools/read/similarity.ts
|
|
17067
|
-
import { z as
|
|
17966
|
+
import { z as z30 } from "zod";
|
|
17068
17967
|
|
|
17069
17968
|
// src/core/read/similarity.ts
|
|
17070
17969
|
import * as fs30 from "fs";
|
|
@@ -17328,9 +18227,9 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
17328
18227
|
title: "Find Similar Notes",
|
|
17329
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.",
|
|
17330
18229
|
inputSchema: {
|
|
17331
|
-
path:
|
|
17332
|
-
limit:
|
|
17333
|
-
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)")
|
|
17334
18233
|
}
|
|
17335
18234
|
},
|
|
17336
18235
|
async ({ path: path33, limit, exclude_linked }) => {
|
|
@@ -17374,7 +18273,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
17374
18273
|
}
|
|
17375
18274
|
|
|
17376
18275
|
// src/tools/read/semantic.ts
|
|
17377
|
-
import { z as
|
|
18276
|
+
import { z as z31 } from "zod";
|
|
17378
18277
|
import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
|
|
17379
18278
|
function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
17380
18279
|
server2.registerTool(
|
|
@@ -17383,7 +18282,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
17383
18282
|
title: "Initialize Semantic Search",
|
|
17384
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.",
|
|
17385
18284
|
inputSchema: {
|
|
17386
|
-
force:
|
|
18285
|
+
force: z31.boolean().optional().describe(
|
|
17387
18286
|
"Rebuild all embeddings even if they already exist (default: false)"
|
|
17388
18287
|
)
|
|
17389
18288
|
}
|
|
@@ -17463,7 +18362,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
17463
18362
|
|
|
17464
18363
|
// src/tools/read/merges.ts
|
|
17465
18364
|
init_levenshtein();
|
|
17466
|
-
import { z as
|
|
18365
|
+
import { z as z32 } from "zod";
|
|
17467
18366
|
import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
|
|
17468
18367
|
function normalizeName(name) {
|
|
17469
18368
|
return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
|
|
@@ -17473,7 +18372,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
17473
18372
|
"suggest_entity_merges",
|
|
17474
18373
|
"Find potential duplicate entities that could be merged based on name similarity",
|
|
17475
18374
|
{
|
|
17476
|
-
limit:
|
|
18375
|
+
limit: z32.number().optional().default(50).describe("Maximum number of suggestions to return")
|
|
17477
18376
|
},
|
|
17478
18377
|
async ({ limit }) => {
|
|
17479
18378
|
const stateDb2 = getStateDb();
|
|
@@ -17575,11 +18474,11 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
17575
18474
|
"dismiss_merge_suggestion",
|
|
17576
18475
|
"Permanently dismiss a merge suggestion so it never reappears",
|
|
17577
18476
|
{
|
|
17578
|
-
source_path:
|
|
17579
|
-
target_path:
|
|
17580
|
-
source_name:
|
|
17581
|
-
target_name:
|
|
17582
|
-
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")
|
|
17583
18482
|
},
|
|
17584
18483
|
async ({ source_path, target_path, source_name, target_name, reason }) => {
|
|
17585
18484
|
const stateDb2 = getStateDb();
|
|
@@ -17740,8 +18639,10 @@ var PRESETS = {
|
|
|
17740
18639
|
"frontmatter",
|
|
17741
18640
|
"notes",
|
|
17742
18641
|
"git",
|
|
17743
|
-
"policy"
|
|
18642
|
+
"policy",
|
|
18643
|
+
"memory"
|
|
17744
18644
|
],
|
|
18645
|
+
agent: ["search", "structure", "append", "frontmatter", "notes", "memory"],
|
|
17745
18646
|
// Composable bundles
|
|
17746
18647
|
graph: ["backlinks", "orphans", "hubs", "paths"],
|
|
17747
18648
|
analysis: ["schema", "wikilinks"],
|
|
@@ -17764,7 +18665,8 @@ var ALL_CATEGORIES = [
|
|
|
17764
18665
|
"frontmatter",
|
|
17765
18666
|
"notes",
|
|
17766
18667
|
"git",
|
|
17767
|
-
"policy"
|
|
18668
|
+
"policy",
|
|
18669
|
+
"memory"
|
|
17768
18670
|
];
|
|
17769
18671
|
var DEFAULT_PRESET = "full";
|
|
17770
18672
|
function parseEnabledCategories() {
|
|
@@ -17868,7 +18770,11 @@ var TOOL_CATEGORY = {
|
|
|
17868
18770
|
suggest_entity_merges: "health",
|
|
17869
18771
|
dismiss_merge_suggestion: "health",
|
|
17870
18772
|
// notes (entity merge)
|
|
17871
|
-
merge_entities: "notes"
|
|
18773
|
+
merge_entities: "notes",
|
|
18774
|
+
// memory (agent working memory)
|
|
18775
|
+
memory: "memory",
|
|
18776
|
+
recall: "memory",
|
|
18777
|
+
brief: "memory"
|
|
17872
18778
|
};
|
|
17873
18779
|
var server = new McpServer({
|
|
17874
18780
|
name: "flywheel-memory",
|
|
@@ -18000,6 +18906,9 @@ registerActivityTools(server, () => stateDb, () => {
|
|
|
18000
18906
|
registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
18001
18907
|
registerSemanticTools(server, () => vaultPath, () => stateDb);
|
|
18002
18908
|
registerMergeTools2(server, () => stateDb);
|
|
18909
|
+
registerMemoryTools(server, () => stateDb);
|
|
18910
|
+
registerRecallTools(server, () => stateDb);
|
|
18911
|
+
registerBriefTools(server, () => stateDb);
|
|
18003
18912
|
registerVaultResources(server, () => vaultIndex ?? null);
|
|
18004
18913
|
serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
18005
18914
|
async function main() {
|