linksee-memory 0.1.4 → 0.3.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.
- package/README.md +162 -4
- package/dist/bin/install-skill.js +9 -5
- package/dist/lib/session-extractor.js +3 -3
- package/dist/mcp/elicitation.d.ts +24 -0
- package/dist/mcp/elicitation.js +58 -0
- package/dist/mcp/prompts.d.ts +21 -0
- package/dist/mcp/prompts.js +201 -0
- package/dist/mcp/resources.d.ts +18 -0
- package/dist/mcp/resources.js +162 -0
- package/dist/mcp/roots.d.ts +10 -0
- package/dist/mcp/roots.js +64 -0
- package/dist/mcp/sampling.d.ts +16 -0
- package/dist/mcp/sampling.js +78 -0
- package/dist/mcp/server.js +255 -68
- package/dist/skill/SKILL.md +168 -162
- package/package.json +82 -71
package/dist/mcp/server.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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';
|
|
@@ -12,10 +12,24 @@ 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
14
|
import { handleReadSmart as handleReadSmartImpl } from './read-smart.js';
|
|
15
|
-
|
|
15
|
+
import { STATIC_RESOURCES, RESOURCE_TEMPLATES, readResource } from './resources.js';
|
|
16
|
+
import { PROMPTS, getPrompt } from './prompts.js';
|
|
17
|
+
import { fetchRoots, isInsideRoots } from './roots.js';
|
|
18
|
+
import { sampleConsolidation } from './sampling.js';
|
|
19
|
+
import { confirmForget } from './elicitation.js';
|
|
20
|
+
const SERVER_VERSION = '0.3.0';
|
|
16
21
|
const db = openDb();
|
|
17
22
|
runMigrations(db);
|
|
18
|
-
const server = new Server({ name: 'linksee-memory', version: SERVER_VERSION }, {
|
|
23
|
+
const server = new Server({ name: 'linksee-memory', version: SERVER_VERSION }, {
|
|
24
|
+
capabilities: {
|
|
25
|
+
tools: {},
|
|
26
|
+
resources: { subscribe: false, listChanged: false },
|
|
27
|
+
prompts: { listChanged: false },
|
|
28
|
+
// Sampling, Roots, Elicitation are CLIENT capabilities the server consumes.
|
|
29
|
+
// We don't declare them under "capabilities" — we just call them via server.request
|
|
30
|
+
// and gracefully degrade when the client doesn't support them.
|
|
31
|
+
},
|
|
32
|
+
});
|
|
19
33
|
// ============================================================
|
|
20
34
|
// Layer alias map — natural language → canonical layer
|
|
21
35
|
// (agents can say layer="decisions" and we resolve to "learning")
|
|
@@ -233,14 +247,14 @@ function toFtsQuery(raw) {
|
|
|
233
247
|
return tokens.map((t) => `"${t}"`).join(' OR ');
|
|
234
248
|
}
|
|
235
249
|
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 ?
|
|
250
|
+
let sql = `
|
|
251
|
+
SELECT m.id, m.entity_id, e.name as entity_name, e.kind as entity_kind, e.momentum_score,
|
|
252
|
+
m.layer, m.content, m.importance, m.created_at, m.last_accessed_at, m.access_count,
|
|
253
|
+
bm25(memories_fts) as bm25_score
|
|
254
|
+
FROM memories_fts
|
|
255
|
+
JOIN memories m ON m.id = memories_fts.rowid
|
|
256
|
+
JOIN entities e ON e.id = m.entity_id
|
|
257
|
+
WHERE memories_fts MATCH ?
|
|
244
258
|
`;
|
|
245
259
|
const params = [query];
|
|
246
260
|
if (layer) {
|
|
@@ -252,13 +266,13 @@ function runFtsQuery(query, layer, limit) {
|
|
|
252
266
|
return db.prepare(sql).all(...params);
|
|
253
267
|
}
|
|
254
268
|
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
|
|
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
|
+
0 as bm25_score
|
|
273
|
+
FROM memories m
|
|
274
|
+
JOIN entities e ON e.id = m.entity_id
|
|
275
|
+
WHERE 1=1
|
|
262
276
|
`;
|
|
263
277
|
const params = [];
|
|
264
278
|
if (entityName) {
|
|
@@ -479,11 +493,14 @@ function handleForget(args) {
|
|
|
479
493
|
return JSON.stringify({ ok: false, error: `memory_id ${args.memory_id} not found` });
|
|
480
494
|
}
|
|
481
495
|
if (target.protected === 1 || target.importance >= 0.9) {
|
|
496
|
+
const isLayerProtected = target.protected === 1;
|
|
482
497
|
return JSON.stringify({
|
|
483
498
|
ok: false,
|
|
484
499
|
preserved: true,
|
|
485
|
-
reason:
|
|
486
|
-
hint:
|
|
500
|
+
reason: isLayerProtected ? `${target.layer}-layer is auto-protected` : 'pinned (importance>=0.9)',
|
|
501
|
+
hint: isLayerProtected
|
|
502
|
+
? `${target.layer} memories are permanently protected (the whole point — pain lessons must not be lost). If you truly need to delete, copy its content to another layer via remember() first, then drop the DB row manually via a SQLite client.`
|
|
503
|
+
: 'Use update_memory to lower importance below 0.9 first, then forget.',
|
|
487
504
|
});
|
|
488
505
|
}
|
|
489
506
|
const res = db.prepare('DELETE FROM memories WHERE id = ?').run(args.memory_id);
|
|
@@ -491,8 +508,8 @@ function handleForget(args) {
|
|
|
491
508
|
}
|
|
492
509
|
// Auto-sweep — also respect pin (importance >= 0.9) as protection
|
|
493
510
|
const rows = db
|
|
494
|
-
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected
|
|
495
|
-
FROM memories
|
|
511
|
+
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected
|
|
512
|
+
FROM memories
|
|
496
513
|
WHERE protected = 0 AND importance < 0.9`)
|
|
497
514
|
.all();
|
|
498
515
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -540,17 +557,17 @@ function handleConsolidate(args) {
|
|
|
540
557
|
// read-only audit: count candidates using the same rules.
|
|
541
558
|
const now = Math.floor(Date.now() / 1000);
|
|
542
559
|
const ageCutoff = now - (args.min_age_days ?? 7) * 86400;
|
|
543
|
-
const candidates = db.prepare(`
|
|
544
|
-
SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
|
|
545
|
-
FROM memories m
|
|
546
|
-
JOIN entities e ON e.id = m.entity_id
|
|
547
|
-
WHERE m.protected = 0
|
|
548
|
-
AND m.importance < 0.9
|
|
549
|
-
AND m.layer IN ('context', 'emotion', 'implementation')
|
|
550
|
-
AND m.created_at <= ?
|
|
551
|
-
GROUP BY m.entity_id, m.layer
|
|
552
|
-
HAVING c >= 2
|
|
553
|
-
ORDER BY c DESC
|
|
560
|
+
const candidates = db.prepare(`
|
|
561
|
+
SELECT m.entity_id, e.name as entity_name, m.layer, COUNT(*) as c
|
|
562
|
+
FROM memories m
|
|
563
|
+
JOIN entities e ON e.id = m.entity_id
|
|
564
|
+
WHERE m.protected = 0
|
|
565
|
+
AND m.importance < 0.9
|
|
566
|
+
AND m.layer IN ('context', 'emotion', 'implementation')
|
|
567
|
+
AND m.created_at <= ?
|
|
568
|
+
GROUP BY m.entity_id, m.layer
|
|
569
|
+
HAVING c >= 2
|
|
570
|
+
ORDER BY c DESC
|
|
554
571
|
`).all(ageCutoff);
|
|
555
572
|
const totalReplaced = candidates.reduce((s, c) => s + c.c, 0);
|
|
556
573
|
return JSON.stringify({
|
|
@@ -624,19 +641,19 @@ function handleListEntities(args) {
|
|
|
624
641
|
const minMemories = Math.max(1, Number(args?.min_memories ?? 1));
|
|
625
642
|
const limit = Math.max(1, Math.min(200, Number(args?.limit ?? 30)));
|
|
626
643
|
const offset = Math.max(0, Number(args?.offset ?? 0));
|
|
627
|
-
let sql = `
|
|
628
|
-
SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
|
|
629
|
-
e.updated_at, e.created_at,
|
|
630
|
-
COUNT(m.id) as memory_count,
|
|
631
|
-
MAX(m.last_accessed_at) as last_memory_access,
|
|
632
|
-
SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
|
|
633
|
-
SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
|
|
634
|
-
SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
|
|
635
|
-
SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
|
|
636
|
-
SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
|
|
637
|
-
FROM entities e
|
|
638
|
-
LEFT JOIN memories m ON m.entity_id = e.id
|
|
639
|
-
WHERE 1=1
|
|
644
|
+
let sql = `
|
|
645
|
+
SELECT e.id, e.name, e.kind, e.canonical_key, e.momentum_score,
|
|
646
|
+
e.updated_at, e.created_at,
|
|
647
|
+
COUNT(m.id) as memory_count,
|
|
648
|
+
MAX(m.last_accessed_at) as last_memory_access,
|
|
649
|
+
SUM(CASE WHEN m.layer = 'goal' THEN 1 ELSE 0 END) as goal_count,
|
|
650
|
+
SUM(CASE WHEN m.layer = 'caveat' THEN 1 ELSE 0 END) as caveat_count,
|
|
651
|
+
SUM(CASE WHEN m.layer = 'learning' THEN 1 ELSE 0 END) as learning_count,
|
|
652
|
+
SUM(CASE WHEN m.layer = 'implementation' THEN 1 ELSE 0 END) as impl_count,
|
|
653
|
+
SUM(CASE WHEN m.importance >= 0.9 THEN 1 ELSE 0 END) as pinned_count
|
|
654
|
+
FROM entities e
|
|
655
|
+
LEFT JOIN memories m ON m.entity_id = e.id
|
|
656
|
+
WHERE 1=1
|
|
640
657
|
`;
|
|
641
658
|
const params = [];
|
|
642
659
|
if (kind) {
|
|
@@ -688,29 +705,29 @@ function handleRecallFile(args) {
|
|
|
688
705
|
return JSON.stringify({ ok: true, count: 0, note: 'No edits found for that path substring.' });
|
|
689
706
|
}
|
|
690
707
|
// Daily breakdown
|
|
691
|
-
const daily = db.prepare(`
|
|
692
|
-
SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
|
|
693
|
-
FROM session_file_edits WHERE file_path LIKE ?
|
|
694
|
-
GROUP BY day, operation ORDER BY day
|
|
708
|
+
const daily = db.prepare(`
|
|
709
|
+
SELECT DATE(occurred_at, 'unixepoch') as day, operation, COUNT(*) as edits
|
|
710
|
+
FROM session_file_edits WHERE file_path LIKE ?
|
|
711
|
+
GROUP BY day, operation ORDER BY day
|
|
695
712
|
`).all(`%${sub}%`);
|
|
696
713
|
// Distinct context_snippets (intents) — deduped, ordered by recency
|
|
697
|
-
const intents = db.prepare(`
|
|
698
|
-
SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
|
|
699
|
-
FROM session_file_edits
|
|
700
|
-
WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
|
|
701
|
-
GROUP BY context_snippet
|
|
702
|
-
ORDER BY last_at DESC
|
|
703
|
-
LIMIT ?
|
|
714
|
+
const intents = db.prepare(`
|
|
715
|
+
SELECT DISTINCT context_snippet, MAX(occurred_at) as last_at, COUNT(*) as freq
|
|
716
|
+
FROM session_file_edits
|
|
717
|
+
WHERE file_path LIKE ? AND context_snippet IS NOT NULL AND LENGTH(context_snippet) > 20
|
|
718
|
+
GROUP BY context_snippet
|
|
719
|
+
ORDER BY last_at DESC
|
|
720
|
+
LIMIT ?
|
|
704
721
|
`).all(`%${sub}%`, maxIntents);
|
|
705
722
|
// Linked memories
|
|
706
|
-
const memories = db.prepare(`
|
|
707
|
-
SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
|
|
708
|
-
FROM session_file_edits sfe
|
|
709
|
-
JOIN memories m ON m.id = sfe.memory_id
|
|
710
|
-
JOIN entities e ON e.id = m.entity_id
|
|
711
|
-
WHERE sfe.file_path LIKE ?
|
|
712
|
-
ORDER BY m.importance DESC
|
|
713
|
-
LIMIT 20
|
|
723
|
+
const memories = db.prepare(`
|
|
724
|
+
SELECT DISTINCT m.id, m.layer, m.content, m.importance, e.name as entity_name
|
|
725
|
+
FROM session_file_edits sfe
|
|
726
|
+
JOIN memories m ON m.id = sfe.memory_id
|
|
727
|
+
JOIN entities e ON e.id = m.entity_id
|
|
728
|
+
WHERE sfe.file_path LIKE ?
|
|
729
|
+
ORDER BY m.importance DESC
|
|
730
|
+
LIMIT 20
|
|
714
731
|
`).all(`%${sub}%`);
|
|
715
732
|
// Distinct file paths matched (the substring may match multiple files)
|
|
716
733
|
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}%`);
|
|
@@ -743,6 +760,158 @@ function handleReadSmart(args) {
|
|
|
743
760
|
return handleReadSmartImpl(db, { path: args.path, force: args.force });
|
|
744
761
|
}
|
|
745
762
|
// ============================================================
|
|
763
|
+
// v0.3.0 — five-blocks helpers (sampling / roots / elicitation in handlers)
|
|
764
|
+
// ============================================================
|
|
765
|
+
async function handleRecallFileWithRoots(args) {
|
|
766
|
+
const baseJson = handleRecallFile(args);
|
|
767
|
+
if (!args?.scope_to_roots)
|
|
768
|
+
return baseJson;
|
|
769
|
+
let parsed;
|
|
770
|
+
try {
|
|
771
|
+
parsed = JSON.parse(baseJson);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return baseJson;
|
|
775
|
+
}
|
|
776
|
+
if (!parsed?.ok || !Array.isArray(parsed.paths_matched))
|
|
777
|
+
return baseJson;
|
|
778
|
+
const roots = await fetchRoots(server);
|
|
779
|
+
if (roots.length === 0) {
|
|
780
|
+
parsed.roots_filter = { applied: false, reason: 'client provided no roots' };
|
|
781
|
+
return JSON.stringify(parsed);
|
|
782
|
+
}
|
|
783
|
+
const filtered = parsed.paths_matched.filter((p) => isInsideRoots(p.file_path, roots));
|
|
784
|
+
parsed.roots_filter = { applied: true, root_count: roots.length, before: parsed.paths_matched.length, after: filtered.length };
|
|
785
|
+
parsed.paths_matched = filtered;
|
|
786
|
+
return JSON.stringify(parsed);
|
|
787
|
+
}
|
|
788
|
+
async function handleConsolidateWithSampling(args) {
|
|
789
|
+
// Sampling only applies on a real run (not dry-run) and only when explicitly opted in.
|
|
790
|
+
if (!args?.use_llm || args?.dry_run)
|
|
791
|
+
return handleConsolidate(args);
|
|
792
|
+
// 1. Snapshot all candidate memories BEFORE consolidate runs so we can recover
|
|
793
|
+
// their content (the originals are deleted by consolidate).
|
|
794
|
+
const ageCutoff = Math.floor(Date.now() / 1000) - (typeof args?.min_age_days === 'number' ? args.min_age_days : 7) * 86400;
|
|
795
|
+
const snapshot = new Map();
|
|
796
|
+
const candidateRows = db
|
|
797
|
+
.prepare(`SELECT m.id, m.content, e.name as entity_name
|
|
798
|
+
FROM memories m JOIN entities e ON e.id = m.entity_id
|
|
799
|
+
WHERE m.protected = 0
|
|
800
|
+
AND m.layer IN ('context','emotion','implementation')
|
|
801
|
+
AND m.created_at <= ?`)
|
|
802
|
+
.all(ageCutoff);
|
|
803
|
+
for (const r of candidateRows)
|
|
804
|
+
snapshot.set(r.id, r);
|
|
805
|
+
// 2. Run the normal heuristic consolidate (creates learning entries, deletes source).
|
|
806
|
+
const baseJson = handleConsolidate(args);
|
|
807
|
+
let parsed;
|
|
808
|
+
try {
|
|
809
|
+
parsed = JSON.parse(baseJson);
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
return baseJson;
|
|
813
|
+
}
|
|
814
|
+
if (!parsed?.ok || !Array.isArray(parsed.learningIdsCreated)) {
|
|
815
|
+
parsed = parsed ?? {};
|
|
816
|
+
parsed.sampling = { applied: false, reason: 'consolidate returned no learning entries' };
|
|
817
|
+
return JSON.stringify(parsed);
|
|
818
|
+
}
|
|
819
|
+
// 3. For each new learning entry, look up its replaced_ids from the audit table,
|
|
820
|
+
// gather source contents from the snapshot, and request a sampled summary.
|
|
821
|
+
let upgraded = 0;
|
|
822
|
+
let declined = 0;
|
|
823
|
+
const declineReasons = [];
|
|
824
|
+
for (const learningId of parsed.learningIdsCreated) {
|
|
825
|
+
const audit = db.prepare('SELECT replaced_ids FROM consolidations WHERE learning_id = ?').get(learningId);
|
|
826
|
+
if (!audit) {
|
|
827
|
+
declined++;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
let replaced;
|
|
831
|
+
try {
|
|
832
|
+
replaced = JSON.parse(audit.replaced_ids);
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
declined++;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (!Array.isArray(replaced) || replaced.length < 2) {
|
|
839
|
+
declined++;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const sources = replaced
|
|
843
|
+
.map((id) => snapshot.get(id))
|
|
844
|
+
.filter((s) => Boolean(s));
|
|
845
|
+
if (sources.length < 2) {
|
|
846
|
+
declined++;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const entityName = sources[0]?.entity_name ?? '<entity>';
|
|
850
|
+
const result = await sampleConsolidation(server, sources.map((s) => s.content), entityName);
|
|
851
|
+
if (result.ok && result.text) {
|
|
852
|
+
db.prepare('UPDATE memories SET content = ? WHERE id = ?').run(result.text.trim(), learningId);
|
|
853
|
+
upgraded++;
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
declined++;
|
|
857
|
+
if (result.reason && declineReasons.length < 3)
|
|
858
|
+
declineReasons.push(result.reason);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
parsed.sampling = {
|
|
862
|
+
applied: true,
|
|
863
|
+
upgraded,
|
|
864
|
+
declined,
|
|
865
|
+
...(declineReasons.length ? { decline_reasons: declineReasons } : {}),
|
|
866
|
+
};
|
|
867
|
+
return JSON.stringify(parsed);
|
|
868
|
+
}
|
|
869
|
+
async function handleForgetInteractive(args) {
|
|
870
|
+
if (!args?.interactive || !args?.memory_id)
|
|
871
|
+
return handleForget(args);
|
|
872
|
+
const id = Number(args.memory_id);
|
|
873
|
+
const row = db
|
|
874
|
+
.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 = ?`)
|
|
875
|
+
.get(id);
|
|
876
|
+
if (!row)
|
|
877
|
+
return JSON.stringify({ ok: false, error: `memory ${id} not found` });
|
|
878
|
+
const ok = await confirmForget(server, {
|
|
879
|
+
id: row.id,
|
|
880
|
+
entity: row.entity,
|
|
881
|
+
layer: row.layer,
|
|
882
|
+
importance: row.importance,
|
|
883
|
+
preview: row.content,
|
|
884
|
+
});
|
|
885
|
+
if (!ok)
|
|
886
|
+
return JSON.stringify({ ok: false, declined: true, memory_id: id, reason: 'user declined elicitation' });
|
|
887
|
+
return handleForget({ memory_id: id });
|
|
888
|
+
}
|
|
889
|
+
// Append new optional flags to existing tools (backward-compatible).
|
|
890
|
+
const RECALL_FILE_TOOL = TOOLS.find((t) => t.name === 'recall_file');
|
|
891
|
+
if (RECALL_FILE_TOOL && RECALL_FILE_TOOL.inputSchema.properties) {
|
|
892
|
+
RECALL_FILE_TOOL.inputSchema.properties.scope_to_roots = {
|
|
893
|
+
type: 'boolean',
|
|
894
|
+
default: false,
|
|
895
|
+
description: 'If true, filter results to files inside the client-provided roots (Roots block). Skip silently when client provides no roots.',
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const CONSOLIDATE_TOOL = TOOLS.find((t) => t.name === 'consolidate');
|
|
899
|
+
if (CONSOLIDATE_TOOL && CONSOLIDATE_TOOL.inputSchema.properties) {
|
|
900
|
+
CONSOLIDATE_TOOL.inputSchema.properties.use_llm = {
|
|
901
|
+
type: 'boolean',
|
|
902
|
+
default: false,
|
|
903
|
+
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.',
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const FORGET_TOOL = TOOLS.find((t) => t.name === 'forget');
|
|
907
|
+
if (FORGET_TOOL && FORGET_TOOL.inputSchema.properties) {
|
|
908
|
+
FORGET_TOOL.inputSchema.properties.interactive = {
|
|
909
|
+
type: 'boolean',
|
|
910
|
+
default: false,
|
|
911
|
+
description: 'If true, ask the user to confirm via Elicitation before deleting. Only applies when memory_id is set.',
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
// ============================================================
|
|
746
915
|
// MCP wiring
|
|
747
916
|
// ============================================================
|
|
748
917
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
@@ -764,13 +933,13 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
764
933
|
text = handleListEntities(args);
|
|
765
934
|
break;
|
|
766
935
|
case 'forget':
|
|
767
|
-
text =
|
|
936
|
+
text = await handleForgetInteractive(args);
|
|
768
937
|
break;
|
|
769
938
|
case 'consolidate':
|
|
770
|
-
text =
|
|
939
|
+
text = await handleConsolidateWithSampling(args);
|
|
771
940
|
break;
|
|
772
941
|
case 'recall_file':
|
|
773
|
-
text =
|
|
942
|
+
text = await handleRecallFileWithRoots(args);
|
|
774
943
|
break;
|
|
775
944
|
case 'read_smart':
|
|
776
945
|
text = handleReadSmart(args);
|
|
@@ -786,6 +955,24 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
786
955
|
};
|
|
787
956
|
}
|
|
788
957
|
});
|
|
958
|
+
// ============================================================
|
|
959
|
+
// Resources block
|
|
960
|
+
// ============================================================
|
|
961
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: STATIC_RESOURCES }));
|
|
962
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: RESOURCE_TEMPLATES }));
|
|
963
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
964
|
+
const { uri } = req.params;
|
|
965
|
+
const result = readResource(db, uri);
|
|
966
|
+
return { contents: [result] };
|
|
967
|
+
});
|
|
968
|
+
// ============================================================
|
|
969
|
+
// Prompts block
|
|
970
|
+
// ============================================================
|
|
971
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
|
|
972
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
973
|
+
const { name, arguments: promptArgs } = req.params;
|
|
974
|
+
return getPrompt(name, promptArgs);
|
|
975
|
+
});
|
|
789
976
|
const transport = new StdioServerTransport();
|
|
790
977
|
await server.connect(transport);
|
|
791
978
|
process.stderr.write(`[linksee-memory] MCP server ready on stdio (v${SERVER_VERSION})\n`);
|