@velvetmonkey/flywheel-memory 2.0.49 → 2.0.51

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