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.
@@ -0,0 +1,162 @@
1
+ // Resources block — expose memories as URI-addressable resources.
2
+ //
3
+ // Static resources (always present):
4
+ // memory://stats — DB statistics summary
5
+ // memory://hot — currently hot memories (top heat band)
6
+ // memory://recent — recently accessed memories (last 7 days)
7
+ // memory://caveats — all caveat-layer memories (the never-forget pile)
8
+ //
9
+ // Resource templates (parameterized):
10
+ // memory://entity/{name} — all memories for an entity
11
+ // memory://layer/{layer} — all memories in a layer (goal/context/emotion/implementation/caveat/learning)
12
+ // memory://memory/{id} — single memory by ID
13
+ export const STATIC_RESOURCES = [
14
+ {
15
+ uri: 'memory://stats',
16
+ name: 'Memory store statistics',
17
+ description: 'Summary counts: entities, memories, layer breakdown, heat distribution.',
18
+ mimeType: 'application/json',
19
+ },
20
+ {
21
+ uri: 'memory://hot',
22
+ name: 'Hot memories',
23
+ description: 'Memories currently in the "hot" heat band — what the agent is actively working with.',
24
+ mimeType: 'application/json',
25
+ },
26
+ {
27
+ uri: 'memory://recent',
28
+ name: 'Recently accessed memories',
29
+ description: 'Memories accessed in the last 7 days, ordered by recency.',
30
+ mimeType: 'application/json',
31
+ },
32
+ {
33
+ uri: 'memory://caveats',
34
+ name: 'All caveats',
35
+ description: 'Every caveat-layer memory — the protected "never forget" pile of pain lessons.',
36
+ mimeType: 'application/json',
37
+ },
38
+ ];
39
+ export const RESOURCE_TEMPLATES = [
40
+ {
41
+ uriTemplate: 'memory://entity/{name}',
42
+ name: 'Memories for an entity',
43
+ description: 'All memories about a specific entity (person/company/project/concept/file). Replace {name} with the entity name.',
44
+ mimeType: 'application/json',
45
+ },
46
+ {
47
+ uriTemplate: 'memory://layer/{layer}',
48
+ name: 'Memories in a layer',
49
+ description: 'All memories in a specific layer. Replace {layer} with one of: goal, context, emotion, implementation, caveat, learning.',
50
+ mimeType: 'application/json',
51
+ },
52
+ {
53
+ uriTemplate: 'memory://memory/{id}',
54
+ name: 'A single memory',
55
+ description: 'Read a single memory by its numeric id. Replace {id} with the memory_id.',
56
+ mimeType: 'application/json',
57
+ },
58
+ ];
59
+ const VALID_LAYERS = new Set(['goal', 'context', 'emotion', 'implementation', 'caveat', 'learning']);
60
+ function fmtMemory(row) {
61
+ return {
62
+ id: row.id,
63
+ entity: row.entity_name,
64
+ entity_kind: row.entity_kind,
65
+ layer: row.layer,
66
+ importance: row.importance,
67
+ pinned: row.protected === 1 || row.importance >= 0.9,
68
+ content: row.content,
69
+ created_at: row.created_at ? new Date(row.created_at * 1000).toISOString() : null,
70
+ last_accessed_at: row.last_accessed_at ? new Date(row.last_accessed_at * 1000).toISOString() : null,
71
+ access_count: row.access_count,
72
+ };
73
+ }
74
+ const SELECT_MEMORY_BASE = `
75
+ SELECT m.id, m.layer, m.content, m.importance, m.protected, m.created_at, m.last_accessed_at, m.access_count,
76
+ e.name as entity_name, e.kind as entity_kind
77
+ FROM memories m JOIN entities e ON e.id = m.entity_id
78
+ `;
79
+ export function readResource(db, uri) {
80
+ // Static endpoints
81
+ if (uri === 'memory://stats') {
82
+ const entityCount = db.prepare('SELECT COUNT(*) as c FROM entities').get().c;
83
+ const memCount = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
84
+ const byLayer = db.prepare('SELECT layer, COUNT(*) as c FROM memories GROUP BY layer').all();
85
+ const byKind = db.prepare('SELECT kind, COUNT(*) as c FROM entities GROUP BY kind').all();
86
+ const pinned = db.prepare('SELECT COUNT(*) as c FROM memories WHERE importance >= 0.9 OR protected = 1').get().c;
87
+ return {
88
+ uri,
89
+ mimeType: 'application/json',
90
+ text: JSON.stringify({
91
+ entity_count: entityCount,
92
+ memory_count: memCount,
93
+ pinned_count: pinned,
94
+ by_layer: Object.fromEntries(byLayer.map((r) => [r.layer, r.c])),
95
+ by_entity_kind: Object.fromEntries(byKind.map((r) => [r.kind, r.c])),
96
+ // Note: heat_band is computed at recall-time, not stored — see recall tool for live heat.
97
+ }, null, 2),
98
+ };
99
+ }
100
+ if (uri === 'memory://hot') {
101
+ // heat_band is dynamic; approximate "hot" via access_count + recent access desc.
102
+ // For exact heat scoring, use the recall tool with band='hot'.
103
+ const rows = db
104
+ .prepare(`${SELECT_MEMORY_BASE}
105
+ WHERE m.last_accessed_at IS NOT NULL
106
+ ORDER BY m.access_count DESC, m.last_accessed_at DESC
107
+ LIMIT 50`)
108
+ .all();
109
+ return {
110
+ uri,
111
+ mimeType: 'application/json',
112
+ text: JSON.stringify({ count: rows.length, note: 'Approximation by access_count + last_accessed_at. Use the recall tool with band="hot" for exact heat scoring.', memories: rows.map(fmtMemory) }, null, 2),
113
+ };
114
+ }
115
+ if (uri === 'memory://recent') {
116
+ const cutoff = Math.floor(Date.now() / 1000) - 7 * 24 * 3600;
117
+ const rows = db.prepare(`${SELECT_MEMORY_BASE} WHERE m.last_accessed_at >= ? ORDER BY m.last_accessed_at DESC LIMIT 50`).all(cutoff);
118
+ return { uri, mimeType: 'application/json', text: JSON.stringify({ count: rows.length, memories: rows.map(fmtMemory) }, null, 2) };
119
+ }
120
+ if (uri === 'memory://caveats') {
121
+ const rows = db.prepare(`${SELECT_MEMORY_BASE} WHERE m.layer = 'caveat' ORDER BY m.importance DESC, m.created_at DESC`).all();
122
+ return { uri, mimeType: 'application/json', text: JSON.stringify({ count: rows.length, memories: rows.map(fmtMemory) }, null, 2) };
123
+ }
124
+ // Templates
125
+ const entityMatch = uri.match(/^memory:\/\/entity\/(.+)$/);
126
+ if (entityMatch) {
127
+ const name = decodeURIComponent(entityMatch[1]);
128
+ const rows = db
129
+ .prepare(`${SELECT_MEMORY_BASE} WHERE LOWER(e.name) = LOWER(?) ORDER BY m.importance DESC, m.created_at DESC`)
130
+ .all(name);
131
+ return {
132
+ uri,
133
+ mimeType: 'application/json',
134
+ text: JSON.stringify({ entity: name, count: rows.length, memories: rows.map(fmtMemory) }, null, 2),
135
+ };
136
+ }
137
+ const layerMatch = uri.match(/^memory:\/\/layer\/(.+)$/);
138
+ if (layerMatch) {
139
+ const layer = decodeURIComponent(layerMatch[1]).toLowerCase();
140
+ if (!VALID_LAYERS.has(layer)) {
141
+ throw new Error(`unknown layer "${layer}". Known: goal, context, emotion, implementation, caveat, learning`);
142
+ }
143
+ const rows = db
144
+ .prepare(`${SELECT_MEMORY_BASE} WHERE m.layer = ? ORDER BY m.importance DESC, m.created_at DESC LIMIT 200`)
145
+ .all(layer);
146
+ return {
147
+ uri,
148
+ mimeType: 'application/json',
149
+ text: JSON.stringify({ layer, count: rows.length, memories: rows.map(fmtMemory) }, null, 2),
150
+ };
151
+ }
152
+ const idMatch = uri.match(/^memory:\/\/memory\/(\d+)$/);
153
+ if (idMatch) {
154
+ const id = Number(idMatch[1]);
155
+ const row = db.prepare(`${SELECT_MEMORY_BASE} WHERE m.id = ?`).get(id);
156
+ if (!row)
157
+ throw new Error(`memory id ${id} not found`);
158
+ return { uri, mimeType: 'application/json', text: JSON.stringify(fmtMemory(row), null, 2) };
159
+ }
160
+ throw new Error(`unknown resource URI: ${uri}`);
161
+ }
162
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1,10 @@
1
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ interface Root {
3
+ uri: string;
4
+ name?: string;
5
+ }
6
+ export declare function fetchRoots(server: Server): Promise<Root[]>;
7
+ export declare function invalidateRootsCache(): void;
8
+ export declare function rootPathFromUri(uri: string): string;
9
+ export declare function isInsideRoots(filePath: string, roots: Root[]): boolean;
10
+ export {};
@@ -0,0 +1,64 @@
1
+ // Roots block — track the client's working roots (directories the agent is operating in).
2
+ //
3
+ // The server pulls roots from the client via server.request({method: 'roots/list'}) and caches them.
4
+ // Used by recall_file and read_smart to bias path-substring matches toward files inside a current root.
5
+ //
6
+ // MCP semantics: client owns the root list, server is informed. We refresh on demand
7
+ // (lazily on first use) and on roots/list_changed notification.
8
+ import { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
9
+ let cachedRoots = null;
10
+ let lastFetched = 0;
11
+ const STALE_MS = 60_000; // re-fetch at most once a minute
12
+ export async function fetchRoots(server) {
13
+ const now = Date.now();
14
+ if (cachedRoots && now - lastFetched < STALE_MS)
15
+ return cachedRoots;
16
+ try {
17
+ const res = await server.request({ method: 'roots/list', params: {} }, ListRootsRequestSchema);
18
+ cachedRoots = Array.isArray(res?.roots) ? res.roots : [];
19
+ lastFetched = now;
20
+ }
21
+ catch {
22
+ // Client may not support roots; treat as empty.
23
+ cachedRoots = [];
24
+ lastFetched = now;
25
+ }
26
+ return cachedRoots ?? [];
27
+ }
28
+ export function invalidateRootsCache() {
29
+ cachedRoots = null;
30
+ lastFetched = 0;
31
+ }
32
+ // Convert "file:///C:/Users/HP/foo" → "C:/Users/HP/foo" (or "/Users/foo" on POSIX)
33
+ export function rootPathFromUri(uri) {
34
+ if (uri.startsWith('file:///')) {
35
+ let p = uri.slice('file:///'.length);
36
+ // Windows: convert "C:/Users/..." (already POSIX-ish) — leave as-is, caller normalizes.
37
+ if (process.platform === 'win32') {
38
+ // also handle "C%3A/" encoding
39
+ p = p.replace(/^([A-Za-z])(?:%3A|:)\//, '$1:/');
40
+ }
41
+ else {
42
+ p = '/' + p;
43
+ }
44
+ try {
45
+ return decodeURIComponent(p);
46
+ }
47
+ catch {
48
+ return p;
49
+ }
50
+ }
51
+ return uri;
52
+ }
53
+ // Returns true if filePath is inside any of the roots (case-insensitive on win32).
54
+ export function isInsideRoots(filePath, roots) {
55
+ if (roots.length === 0)
56
+ return true; // no roots → no filtering
57
+ const norm = process.platform === 'win32' ? filePath.toLowerCase().replace(/\\/g, '/') : filePath;
58
+ return roots.some((r) => {
59
+ const rp = rootPathFromUri(r.uri);
60
+ const rpn = process.platform === 'win32' ? rp.toLowerCase().replace(/\\/g, '/') : rp;
61
+ return norm.startsWith(rpn);
62
+ });
63
+ }
64
+ //# sourceMappingURL=roots.js.map
@@ -0,0 +1,16 @@
1
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ interface SampleParams {
3
+ systemPrompt?: string;
4
+ userPrompt: string;
5
+ maxTokens?: number;
6
+ temperature?: number;
7
+ }
8
+ interface SampleResult {
9
+ ok: boolean;
10
+ text?: string;
11
+ reason?: string;
12
+ }
13
+ export declare function sample(server: Server, p: SampleParams): Promise<SampleResult>;
14
+ export declare function sampleConsolidation(server: Server, memoryTexts: string[], entityName: string): Promise<SampleResult>;
15
+ export declare function sampleLayerClassification(server: Server, content: string): Promise<SampleResult>;
16
+ export {};
@@ -0,0 +1,78 @@
1
+ // Sampling block — server requests text generation from the client's LLM.
2
+ //
3
+ // MCP semantics: server.request({method: 'sampling/createMessage', params: {messages, ...}})
4
+ // Used by:
5
+ // - LLM-assisted consolidate (auto-summarize a memory cluster into a learning entry)
6
+ // - LLM-assisted layer classification (when the agent uses a vague layer alias)
7
+ //
8
+ // The client may decline (no LLM access, user opted out). All sampling calls are best-effort
9
+ // with a graceful fallback to non-LLM behavior.
10
+ import { CreateMessageRequestSchema } from '@modelcontextprotocol/sdk/types.js';
11
+ export async function sample(server, p) {
12
+ try {
13
+ const params = {
14
+ messages: [{ role: 'user', content: { type: 'text', text: p.userPrompt } }],
15
+ maxTokens: p.maxTokens ?? 800,
16
+ temperature: p.temperature ?? 0.3,
17
+ modelPreferences: {
18
+ // Prefer fast, cheap, good-enough for summarization.
19
+ speedPriority: 0.7,
20
+ costPriority: 0.7,
21
+ intelligencePriority: 0.4,
22
+ },
23
+ includeContext: 'none',
24
+ };
25
+ if (p.systemPrompt)
26
+ params.systemPrompt = p.systemPrompt;
27
+ const res = await server.request({ method: 'sampling/createMessage', params }, CreateMessageRequestSchema);
28
+ const text = res?.content?.text ?? res?.content?.[0]?.text ?? '';
29
+ if (!text)
30
+ return { ok: false, reason: 'empty response from sampling' };
31
+ return { ok: true, text };
32
+ }
33
+ catch (err) {
34
+ return { ok: false, reason: err?.message ?? String(err) };
35
+ }
36
+ }
37
+ export async function sampleConsolidation(server, memoryTexts, entityName) {
38
+ if (memoryTexts.length === 0)
39
+ return { ok: false, reason: 'no input memories' };
40
+ const numbered = memoryTexts.map((t, i) => `[${i + 1}] ${t}`).join('\n\n');
41
+ const userPrompt = `Consolidate these ${memoryTexts.length} memories about "${entityName}" into a single learning-layer summary. The summary must:
42
+ - Preserve the trajectory (what was attempted, what worked, what changed)
43
+ - Be 2-4 sentences max
44
+ - Not invent facts not in the originals
45
+ - Not include verbatim quotes
46
+
47
+ Memories:
48
+ ${numbered}
49
+
50
+ Output the summary text only — no preamble, no JSON wrapper.`;
51
+ return sample(server, {
52
+ systemPrompt: 'You are a memory consolidator. Compress without distorting.',
53
+ userPrompt,
54
+ maxTokens: 400,
55
+ temperature: 0.2,
56
+ });
57
+ }
58
+ export async function sampleLayerClassification(server, content) {
59
+ const userPrompt = `Classify this memory content into exactly one layer:
60
+ - goal: WHY this work exists, target outcome
61
+ - context: WHY THIS NOW, situation, timing
62
+ - emotion: USER tone, feelings expressed
63
+ - implementation: HOW it was done, what worked, what failed
64
+ - caveat: PAIN lesson, "never X" / "always Y"
65
+ - learning: GROWTH, decisions made, insights
66
+
67
+ Content:
68
+ ${content}
69
+
70
+ Output just the layer name — no explanation, no JSON.`;
71
+ return sample(server, {
72
+ systemPrompt: 'You are a layer classifier. Output one word.',
73
+ userPrompt,
74
+ maxTokens: 16,
75
+ temperature: 0,
76
+ });
77
+ }
78
+ //# sourceMappingURL=sampling.js.map