@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.
Files changed (2) hide show
  1. package/dist/index.js +1095 -186
  2. 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 z23 } from "zod";
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: z23.enum(["get", "set"]).describe("Operation mode"),
16404
- key: z23.string().optional().describe("Config key to update (required for set mode)"),
16405
- value: z23.unknown().optional().describe("New value for the key (required for set mode)")
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 z24 } from "zod";
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: z24.boolean().default(true).describe("If true (default), preview what would be linked without modifying files"),
16499
- batch_size: z24.number().default(50).describe("Maximum notes to process per invocation (default: 50)"),
16500
- offset: z24.number().default(0).describe("Skip this many eligible notes (for pagination across invocations)")
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 z25 } from "zod";
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: z25.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
16762
- metric: z25.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
16763
- days_back: z25.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
16764
- limit: z25.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
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 z26 } from "zod";
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: z26.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
16998
- session_id: z26.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
16999
- days_back: z26.number().optional().describe("Number of days to look back (default: 30)"),
17000
- limit: z26.number().optional().describe("Maximum results to return (default: 20)")
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 z27 } from "zod";
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: z27.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
17332
- limit: z27.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
17333
- exclude_linked: z27.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
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 z28 } from "zod";
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: z28.boolean().optional().describe(
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 z29 } from "zod";
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: z29.number().optional().default(50).describe("Maximum number of suggestions to return")
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: z29.string().describe("Path of the source entity"),
17579
- target_path: z29.string().describe("Path of the target entity"),
17580
- source_name: z29.string().describe("Name of the source entity"),
17581
- target_name: z29.string().describe("Name of the target entity"),
17582
- reason: z29.string().describe("Original suggestion 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() {