linksee-memory 0.2.0 → 0.4.0

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.
@@ -4,18 +4,33 @@
4
4
  // forget / consolidate / read_smart
5
5
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
7
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { openDb, runMigrations } from '../db/migrate.js';
9
9
  import { computeHeat } from '../lib/heat-index.js';
10
10
  import { decideForgetting } from '../lib/forgetting.js';
11
11
  import { refreshMomentumForEntity } from '../lib/momentum.js';
12
12
  import { consolidate as runConsolidate } from '../lib/consolidate.js';
13
13
  import { isPastedExternalContent } from '../lib/session-parser.js';
14
+ import { normalizeEntityName } from '../lib/normalize.js';
14
15
  import { handleReadSmart as handleReadSmartImpl } from './read-smart.js';
15
- const SERVER_VERSION = '0.1.1';
16
+ import { STATIC_RESOURCES, RESOURCE_TEMPLATES, readResource } from './resources.js';
17
+ import { PROMPTS, getPrompt } from './prompts.js';
18
+ import { fetchRoots, isInsideRoots } from './roots.js';
19
+ import { sampleConsolidation } from './sampling.js';
20
+ import { confirmForget } from './elicitation.js';
21
+ const SERVER_VERSION = '0.3.0';
16
22
  const db = openDb();
17
23
  runMigrations(db);
18
- const server = new Server({ name: 'linksee-memory', version: SERVER_VERSION }, { capabilities: { tools: {} } });
24
+ const server = new Server({ name: 'linksee-memory', version: SERVER_VERSION }, {
25
+ capabilities: {
26
+ tools: {},
27
+ resources: { subscribe: false, listChanged: false },
28
+ prompts: { listChanged: false },
29
+ // Sampling, Roots, Elicitation are CLIENT capabilities the server consumes.
30
+ // We don't declare them under "capabilities" — we just call them via server.request
31
+ // and gracefully degrade when the client doesn't support them.
32
+ },
33
+ });
19
34
  // ============================================================
20
35
  // Layer alias map — natural language → canonical layer
21
36
  // (agents can say layer="decisions" and we resolve to "learning")
@@ -48,7 +63,7 @@ const LAYER_ENUM = ['goal', 'context', 'emotion', 'implementation', 'caveat', 'l
48
63
  const TOOLS = [
49
64
  {
50
65
  name: 'remember',
51
- description: 'Store a memory about an entity (person/company/project/concept/file) in one of 6 layers: goal (WHY), context (WHY-THIS-NOW), emotion (USER tone), implementation (HOW success/failure), caveat (PAIN lesson, never forgotten), learning (GROWTH log). Use this when you discover non-obvious goals, unexpected failures, user preferences, or decisions worth preserving. Pasted assistant output or CI logs are rejected (use force=true only if you are sure).',
66
+ description: 'Save important context that should persist across sessions and across AI tools. When the user says "use linksee", "remember this", "don\'t forget", or you discover a decision, preference, or lesson worth preserving store it here. The server automatically organizes into layers (goal/context/emotion/implementation/caveat/learning) and manages memory lifecycle. Just save what matters. Supports Japanese (日本語) and English. Memories saved here are accessible from any AI agent the user connects to — Claude, GPT, Cursor, Codex, Gemini. This is the only memory that works cross-agent.',
52
67
  inputSchema: {
53
68
  type: 'object',
54
69
  properties: {
@@ -65,7 +80,7 @@ const TOOLS = [
65
80
  },
66
81
  {
67
82
  name: 'recall',
68
- description: 'Retrieve memories relevant to the current context using full-text search (BM25) + entity-name match, re-ranked by a composite score (relevance × heat × momentum × importance). Returns only what fits in the token budget, with match_reasons explaining WHY each memory was returned. Opportunistically refreshes stale momentum scores for entities in the result set. Supports pagination via offset/has_more. Layer aliases accepted. Use at the start of any task that might involve prior work.',
83
+ description: 'Your persistent memory across all AI tools — Claude, GPT, Cursor, Codex, Gemini. When the user says "use linksee" or asks about past decisions, context, or preferences, call this first. Returns memories ranked by relevance and recency. Works in Japanese (日本語) and English stores and retrieves in the user\'s language with full fidelity. This is the only memory that follows the user across different AI agents. Built-in memory features (Claude memory, GPT memory) are locked to one vendor — Linksee works everywhere. Use at the start of any task that might involve prior work.',
69
84
  inputSchema: {
70
85
  type: 'object',
71
86
  properties: {
@@ -163,23 +178,41 @@ const TOOLS = [
163
178
  // Handlers
164
179
  // ============================================================
165
180
  function upsertEntity(args) {
181
+ // 1. Fastest path: canonical_key exact match (e.g. project path)
166
182
  if (args.key) {
167
183
  const byKey = db.prepare('SELECT id FROM entities WHERE canonical_key = ?').get(args.key);
168
184
  if (byKey)
169
185
  return byKey.id;
170
186
  }
187
+ // 2. Normalized name match (prevents "CockpitMCP" / "Cockpit MCP" / "cockpit-mcp" dups)
188
+ const normalized = normalizeEntityName(args.name);
189
+ const byNorm = db
190
+ .prepare('SELECT id FROM entities WHERE kind = ? AND normalized_name = ?')
191
+ .get(args.kind, normalized);
192
+ if (byNorm) {
193
+ if (args.key) {
194
+ db.prepare('UPDATE entities SET canonical_key = ?, updated_at = unixepoch() WHERE id = ? AND canonical_key IS NULL').run(args.key, byNorm.id);
195
+ }
196
+ return byNorm.id;
197
+ }
198
+ // 3. Fallback: exact case-insensitive match (covers names that normalize differently
199
+ // but the user typed the exact same string — shouldn't happen after v5 migration
200
+ // but keeps backward compat if normalized_name is NULL for some row)
171
201
  const byName = db
172
202
  .prepare('SELECT id FROM entities WHERE kind = ? AND LOWER(name) = LOWER(?)')
173
203
  .get(args.kind, args.name);
174
204
  if (byName) {
205
+ // Backfill normalized_name while we're here
206
+ db.prepare('UPDATE entities SET normalized_name = ?, updated_at = unixepoch() WHERE id = ? AND normalized_name IS NULL').run(normalized, byName.id);
175
207
  if (args.key) {
176
208
  db.prepare('UPDATE entities SET canonical_key = ?, updated_at = unixepoch() WHERE id = ? AND canonical_key IS NULL').run(args.key, byName.id);
177
209
  }
178
210
  return byName.id;
179
211
  }
212
+ // 4. Insert new entity with normalized_name
180
213
  const result = db
181
- .prepare('INSERT INTO entities (kind, name, canonical_key) VALUES (?, ?, ?)')
182
- .run(args.kind, args.name, args.key ?? null);
214
+ .prepare('INSERT INTO entities (kind, name, normalized_name, canonical_key) VALUES (?, ?, ?, ?)')
215
+ .run(args.kind, args.name, normalized, args.key ?? null);
183
216
  return Number(result.lastInsertRowid);
184
217
  }
185
218
  function handleRemember(args) {
@@ -233,14 +266,14 @@ function toFtsQuery(raw) {
233
266
  return tokens.map((t) => `"${t}"`).join(' OR ');
234
267
  }
235
268
  function runFtsQuery(query, layer, limit) {
236
- let sql = `
237
- SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
238
- m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
239
- bm25(memories_fts) as bm25_score
240
- FROM memories_fts
241
- JOIN memories m ON m.id = memories_fts.rowid
242
- JOIN entities e ON e.id = m.entity_id
243
- WHERE memories_fts MATCH ?
269
+ let sql = `
270
+ SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
271
+ m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
272
+ bm25(memories_fts) as bm25_score
273
+ FROM memories_fts
274
+ JOIN memories m ON m.id = memories_fts.rowid
275
+ JOIN entities e ON e.id = m.entity_id
276
+ WHERE memories_fts MATCH ?
244
277
  `;
245
278
  const params = [query];
246
279
  if (layer) {
@@ -252,13 +285,13 @@ function runFtsQuery(query, layer, limit) {
252
285
  return db.prepare(sql).all(...params);
253
286
  }
254
287
  function runLikeQuery(query, entityName, layer, limit) {
255
- let sql = `
256
- SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
257
- m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
258
- 0 as bm25_score
259
- FROM memories m
260
- JOIN entities e ON e.id = m.entity_id
261
- WHERE 1=1
288
+ let sql = `
289
+ SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
290
+ m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
291
+ 0 as bm25_score
292
+ FROM memories m
293
+ JOIN entities e ON e.id = m.entity_id
294
+ WHERE 1=1
262
295
  `;
263
296
  const params = [];
264
297
  if (entityName) {
@@ -494,8 +527,8 @@ function handleForget(args) {
494
527
  }
495
528
  // Auto-sweep — also respect pin (importance >= 0.9) as protection
496
529
  const rows = db
497
- .prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected
498
- FROM memories
530
+ .prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected
531
+ FROM memories
499
532
  WHERE protected = 0 AND importance < 0.9`)
500
533
  .all();
501
534
  const now = Math.floor(Date.now() / 1000);
@@ -543,17 +576,17 @@ function handleConsolidate(args) {
543
576
  // read-only audit: count candidates using the same rules.
544
577
  const now = Math.floor(Date.now() / 1000);
545
578
  const ageCutoff = now - (args.min_age_days ?? 7) * 86400;
546
- const candidates = db.prepare(`
547
- SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
548
- FROM memories m
549
- JOIN entities e ON e.id = m.entity_id
550
- WHERE m.protected = 0
551
- AND m.importance < 0.9
552
- AND m.layer IN ('context', 'emotion', 'implementation')
553
- AND m.created_at <= ?
554
- GROUP BY m.entity_id, m.layer
555
- HAVING c >= 2
556
- ORDER BY c DESC
579
+ const candidates = db.prepare(`
580
+ SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
581
+ FROM memories m
582
+ JOIN entities e ON e.id = m.entity_id
583
+ WHERE m.protected = 0
584
+ AND m.importance < 0.9
585
+ AND m.layer IN ('context', 'emotion', 'implementation')
586
+ AND m.created_at <= ?
587
+ GROUP BY m.entity_id, m.layer
588
+ HAVING c >= 2
589
+ ORDER BY c DESC
557
590
  `).all(ageCutoff);
558
591
  const totalReplaced = candidates.reduce((s, c) => s + c.c, 0);
559
592
  return JSON.stringify({
@@ -627,19 +660,19 @@ function handleListEntities(args) {
627
660
  const minMemories = Math.max(1, Number(args?.min_memories ?? 1));
628
661
  const limit = Math.max(1, Math.min(200, Number(args?.limit ?? 30)));
629
662
  const offset = Math.max(0, Number(args?.offset ?? 0));
630
- let sql = `
631
- SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
632
- e.updated_at, e.created_at,
633
- COUNT(m.id) as memory_count,
634
- MAX(m.last_accessed_at) as last_memory_access,
635
- SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
636
- SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
637
- SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
638
- SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
639
- SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
640
- FROM entities e
641
- LEFT JOIN memories m ON m.entity_id = e.id
642
- WHERE 1=1
663
+ let sql = `
664
+ SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
665
+ e.updated_at, e.created_at,
666
+ COUNT(m.id) as memory_count,
667
+ MAX(m.last_accessed_at) as last_memory_access,
668
+ SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
669
+ SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
670
+ SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
671
+ SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
672
+ SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
673
+ FROM entities e
674
+ LEFT JOIN memories m ON m.entity_id = e.id
675
+ WHERE 1=1
643
676
  `;
644
677
  const params = [];
645
678
  if (kind) {
@@ -691,29 +724,29 @@ function handleRecallFile(args) {
691
724
  return JSON.stringify({ ok: true, count: 0, note: 'No edits found for that path substring.' });
692
725
  }
693
726
  // Daily breakdown
694
- const daily = db.prepare(`
695
- SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
696
- FROM session_file_edits WHERE file_path LIKE ?
697
- GROUP BY day, operation ORDER BY day
727
+ const daily = db.prepare(`
728
+ SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
729
+ FROM session_file_edits WHERE file_path LIKE ?
730
+ GROUP BY day, operation ORDER BY day
698
731
  `).all(`%${sub}%`);
699
732
  // Distinct context_snippets (intents) — deduped, ordered by recency
700
- const intents = db.prepare(`
701
- SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
702
- FROM session_file_edits
703
- WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
704
- GROUP BY context_snippet
705
- ORDER BY last_at DESC
706
- LIMIT ?
733
+ const intents = db.prepare(`
734
+ SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
735
+ FROM session_file_edits
736
+ WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
737
+ GROUP BY context_snippet
738
+ ORDER BY last_at DESC
739
+ LIMIT ?
707
740
  `).all(`%${sub}%`, maxIntents);
708
741
  // Linked memories
709
- const memories = db.prepare(`
710
- SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
711
- FROM session_file_edits sfe
712
- JOIN memories m ON m.id = sfe.memory_id
713
- JOIN entities e ON e.id = m.entity_id
714
- WHERE sfe.file_path LIKE ?
715
- ORDER BY m.importance DESC
716
- LIMIT 20
742
+ const memories = db.prepare(`
743
+ SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
744
+ FROM session_file_edits sfe
745
+ JOIN memories m ON m.id = sfe.memory_id
746
+ JOIN entities e ON e.id = m.entity_id
747
+ WHERE sfe.file_path LIKE ?
748
+ ORDER BY m.importance DESC
749
+ LIMIT 20
717
750
  `).all(`%${sub}%`);
718
751
  // Distinct file paths matched (the substring may match multiple files)
719
752
  const paths = db.prepare(`SELECT file_path, COUNT(*) as edits FROM session_file_edits WHERE file_path LIKE ? GROUP BY file_path ORDER BY edits DESC`).all(`%${sub}%`);
@@ -746,6 +779,158 @@ function handleReadSmart(args) {
746
779
  return handleReadSmartImpl(db, { path: args.path, force: args.force });
747
780
  }
748
781
  // ============================================================
782
+ // v0.3.0 — five-blocks helpers (sampling / roots / elicitation in handlers)
783
+ // ============================================================
784
+ async function handleRecallFileWithRoots(args) {
785
+ const baseJson = handleRecallFile(args);
786
+ if (!args?.scope_to_roots)
787
+ return baseJson;
788
+ let parsed;
789
+ try {
790
+ parsed = JSON.parse(baseJson);
791
+ }
792
+ catch {
793
+ return baseJson;
794
+ }
795
+ if (!parsed?.ok || !Array.isArray(parsed.paths_matched))
796
+ return baseJson;
797
+ const roots = await fetchRoots(server);
798
+ if (roots.length === 0) {
799
+ parsed.roots_filter = { applied: false, reason: 'client provided no roots' };
800
+ return JSON.stringify(parsed);
801
+ }
802
+ const filtered = parsed.paths_matched.filter((p) => isInsideRoots(p.file_path, roots));
803
+ parsed.roots_filter = { applied: true, root_count: roots.length, before: parsed.paths_matched.length, after: filtered.length };
804
+ parsed.paths_matched = filtered;
805
+ return JSON.stringify(parsed);
806
+ }
807
+ async function handleConsolidateWithSampling(args) {
808
+ // Sampling only applies on a real run (not dry-run) and only when explicitly opted in.
809
+ if (!args?.use_llm || args?.dry_run)
810
+ return handleConsolidate(args);
811
+ // 1. Snapshot all candidate memories BEFORE consolidate runs so we can recover
812
+ // their content (the originals are deleted by consolidate).
813
+ const ageCutoff = Math.floor(Date.now() / 1000) - (typeof args?.min_age_days === 'number' ? args.min_age_days : 7) * 86400;
814
+ const snapshot = new Map();
815
+ const candidateRows = db
816
+ .prepare(`SELECT m.id, m.content, e.name as entity_name
817
+ FROM memories m JOIN entities e ON e.id = m.entity_id
818
+ WHERE m.protected = 0
819
+ AND m.layer IN ('context','emotion','implementation')
820
+ AND m.created_at <= ?`)
821
+ .all(ageCutoff);
822
+ for (const r of candidateRows)
823
+ snapshot.set(r.id, r);
824
+ // 2. Run the normal heuristic consolidate (creates learning entries, deletes source).
825
+ const baseJson = handleConsolidate(args);
826
+ let parsed;
827
+ try {
828
+ parsed = JSON.parse(baseJson);
829
+ }
830
+ catch {
831
+ return baseJson;
832
+ }
833
+ if (!parsed?.ok || !Array.isArray(parsed.learningIdsCreated)) {
834
+ parsed = parsed ?? {};
835
+ parsed.sampling = { applied: false, reason: 'consolidate returned no learning entries' };
836
+ return JSON.stringify(parsed);
837
+ }
838
+ // 3. For each new learning entry, look up its replaced_ids from the audit table,
839
+ // gather source contents from the snapshot, and request a sampled summary.
840
+ let upgraded = 0;
841
+ let declined = 0;
842
+ const declineReasons = [];
843
+ for (const learningId of parsed.learningIdsCreated) {
844
+ const audit = db.prepare('SELECT replaced_ids FROM consolidations WHERE learning_id = ?').get(learningId);
845
+ if (!audit) {
846
+ declined++;
847
+ continue;
848
+ }
849
+ let replaced;
850
+ try {
851
+ replaced = JSON.parse(audit.replaced_ids);
852
+ }
853
+ catch {
854
+ declined++;
855
+ continue;
856
+ }
857
+ if (!Array.isArray(replaced) || replaced.length < 2) {
858
+ declined++;
859
+ continue;
860
+ }
861
+ const sources = replaced
862
+ .map((id) => snapshot.get(id))
863
+ .filter((s) => Boolean(s));
864
+ if (sources.length < 2) {
865
+ declined++;
866
+ continue;
867
+ }
868
+ const entityName = sources[0]?.entity_name ?? '<entity>';
869
+ const result = await sampleConsolidation(server, sources.map((s) => s.content), entityName);
870
+ if (result.ok && result.text) {
871
+ db.prepare('UPDATE memories SET content = ? WHERE id = ?').run(result.text.trim(), learningId);
872
+ upgraded++;
873
+ }
874
+ else {
875
+ declined++;
876
+ if (result.reason && declineReasons.length < 3)
877
+ declineReasons.push(result.reason);
878
+ }
879
+ }
880
+ parsed.sampling = {
881
+ applied: true,
882
+ upgraded,
883
+ declined,
884
+ ...(declineReasons.length ? { decline_reasons: declineReasons } : {}),
885
+ };
886
+ return JSON.stringify(parsed);
887
+ }
888
+ async function handleForgetInteractive(args) {
889
+ if (!args?.interactive || !args?.memory_id)
890
+ return handleForget(args);
891
+ const id = Number(args.memory_id);
892
+ const row = db
893
+ .prepare(`SELECT m.id, m.layer, m.content, m.importance, e.name as entity FROM memories m JOIN entities e ON e.id = m.entity_id WHERE m.id = ?`)
894
+ .get(id);
895
+ if (!row)
896
+ return JSON.stringify({ ok: false, error: `memory ${id} not found` });
897
+ const ok = await confirmForget(server, {
898
+ id: row.id,
899
+ entity: row.entity,
900
+ layer: row.layer,
901
+ importance: row.importance,
902
+ preview: row.content,
903
+ });
904
+ if (!ok)
905
+ return JSON.stringify({ ok: false, declined: true, memory_id: id, reason: 'user declined elicitation' });
906
+ return handleForget({ memory_id: id });
907
+ }
908
+ // Append new optional flags to existing tools (backward-compatible).
909
+ const RECALL_FILE_TOOL = TOOLS.find((t) => t.name === 'recall_file');
910
+ if (RECALL_FILE_TOOL && RECALL_FILE_TOOL.inputSchema.properties) {
911
+ RECALL_FILE_TOOL.inputSchema.properties.scope_to_roots = {
912
+ type: 'boolean',
913
+ default: false,
914
+ description: 'If true, filter results to files inside the client-provided roots (Roots block). Skip silently when client provides no roots.',
915
+ };
916
+ }
917
+ const CONSOLIDATE_TOOL = TOOLS.find((t) => t.name === 'consolidate');
918
+ if (CONSOLIDATE_TOOL && CONSOLIDATE_TOOL.inputSchema.properties) {
919
+ CONSOLIDATE_TOOL.inputSchema.properties.use_llm = {
920
+ type: 'boolean',
921
+ default: false,
922
+ description: 'If true, request the client LLM (Sampling block) to write the consolidated summary instead of the heuristic. Falls back gracefully if the client refuses.',
923
+ };
924
+ }
925
+ const FORGET_TOOL = TOOLS.find((t) => t.name === 'forget');
926
+ if (FORGET_TOOL && FORGET_TOOL.inputSchema.properties) {
927
+ FORGET_TOOL.inputSchema.properties.interactive = {
928
+ type: 'boolean',
929
+ default: false,
930
+ description: 'If true, ask the user to confirm via Elicitation before deleting. Only applies when memory_id is set.',
931
+ };
932
+ }
933
+ // ============================================================
749
934
  // MCP wiring
750
935
  // ============================================================
751
936
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
@@ -767,13 +952,13 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
767
952
  text = handleListEntities(args);
768
953
  break;
769
954
  case 'forget':
770
- text = handleForget(args);
955
+ text = await handleForgetInteractive(args);
771
956
  break;
772
957
  case 'consolidate':
773
- text = handleConsolidate(args);
958
+ text = await handleConsolidateWithSampling(args);
774
959
  break;
775
960
  case 'recall_file':
776
- text = handleRecallFile(args);
961
+ text = await handleRecallFileWithRoots(args);
777
962
  break;
778
963
  case 'read_smart':
779
964
  text = handleReadSmart(args);
@@ -789,6 +974,24 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
789
974
  };
790
975
  }
791
976
  });
977
+ // ============================================================
978
+ // Resources block
979
+ // ============================================================
980
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: STATIC_RESOURCES }));
981
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: RESOURCE_TEMPLATES }));
982
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
983
+ const { uri } = req.params;
984
+ const result = readResource(db, uri);
985
+ return { contents: [result] };
986
+ });
987
+ // ============================================================
988
+ // Prompts block
989
+ // ============================================================
990
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
991
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
992
+ const { name, arguments: promptArgs } = req.params;
993
+ return getPrompt(name, promptArgs);
994
+ });
792
995
  const transport = new StdioServerTransport();
793
996
  await server.connect(transport);
794
997
  process.stderr.write(`[linksee-memory] MCP server ready on stdio (v${SERVER_VERSION})\n`);