linksee-memory 0.2.0 → 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 +15 -4
- 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 +250 -66
- package/package.json +82 -71
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Local-first agent memory MCP. A cross-agent brain for Claude Code, Cursor, and ChatGPT Desktop — with a token-saving file diff cache that nobody else does.
|
|
4
4
|
>
|
|
5
|
-
> **v0.
|
|
5
|
+
> **v0.3.0** ships the **Five Blocks**: Tools + Resources + Prompts + Sampling + Roots, plus the newer **Elicitation** primitive. Most public MCP servers expose only Tools; v0.3.0 moves linksee-memory into the differentiated tier. Backward compatible — all 8 v0.2.x tools keep their signatures. See [CHANGELOG.md](./CHANGELOG.md).
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/linksee-memory)
|
|
8
8
|
[](./LICENSE)
|
|
@@ -147,18 +147,29 @@ Add to `~/.claude/settings.json` to record every Claude Code session to your loc
|
|
|
147
147
|
|
|
148
148
|
Each turn end takes ~100 ms. Failures are silent (Claude Code never blocks). Logs at `~/.linksee-memory/hook.log`.
|
|
149
149
|
|
|
150
|
+
## v0.3.0 — Five Blocks at a glance
|
|
151
|
+
|
|
152
|
+
| MCP Block | Surface |
|
|
153
|
+
|---|---|
|
|
154
|
+
| **Tools** | 8 tools (unchanged signatures since v0.2). |
|
|
155
|
+
| **Resources** | 4 static URIs (`memory://stats`, `memory://hot`, `memory://recent`, `memory://caveats`) + 3 templates (`memory://entity/{name}`, `memory://layer/{layer}`, `memory://memory/{id}`). Browseable via `@-mention` in clients that support it. |
|
|
156
|
+
| **Prompts** | 5 reusable templates: `summarize-session`, `extract-caveats`, `weekly-consolidation`, `recall-and-write`, `entity-handoff`. |
|
|
157
|
+
| **Sampling** *(client opt-in)* | `consolidate{use_llm:true}` asks the client LLM to rewrite consolidated cluster summaries into prose. Falls back to the heuristic when the client declines. |
|
|
158
|
+
| **Roots** *(client opt-in)* | `recall_file{scope_to_roots:true}` filters path matches to files inside any client-provided working root. |
|
|
159
|
+
| **Elicitation** *(client opt-in, newer primitive)* | `forget{interactive:true, memory_id:N}` asks the user to confirm via the client UI before deleting. |
|
|
160
|
+
|
|
150
161
|
## Tools
|
|
151
162
|
|
|
152
163
|
| Tool | Purpose |
|
|
153
164
|
|---|---|
|
|
154
165
|
| `remember` | Store memory in 1 of 6 layers for an entity. Rejects pasted assistant output / CI logs unless `force=true`. Set `importance=1.0` to pin (survives auto-forget). |
|
|
155
166
|
| `recall` | FTS5 + heat × momentum × importance composite ranking with `match_reasons` explaining WHY each row matched. Supports pagination (`offset`/`has_more`), `band` filter, layer aliases (`decisions`/`warnings`/`how`/...), and `mark_accessed=false` for passive previews. |
|
|
156
|
-
| `recall_file` | Complete edit history of a file across all sessions, with per-edit user-intent context. |
|
|
167
|
+
| `recall_file` | Complete edit history of a file across all sessions, with per-edit user-intent context. **v0.3.0** `scope_to_roots` flag filters by client roots. |
|
|
157
168
|
| `update_memory` | **v0.1.0** Atomic edit of an existing memory. Preserves `memory_id` (session_file_edits links stay intact). Prefer over forget+remember. |
|
|
158
169
|
| `list_entities` | **v0.1.0** List what the memory knows about — cheapest "what do I know?" primitive. Filter by `kind`/`min_memories`; returns layer breakdown per entity. |
|
|
159
170
|
| `read_smart` | Diff-only file read. Returns full content on first read, ~50 tokens on unchanged re-reads, only changed chunks on real edits. |
|
|
160
|
-
| `forget` | Explicit delete OR auto-sweep based on `forgettingRisk`. Pinned (`importance>=1.0`) and caveat-layer memories are always preserved. |
|
|
161
|
-
| `consolidate` | Sleep-mode compression: cluster cold low-importance memories → protected learning-layer summary. Supports `dry_run` preview. |
|
|
171
|
+
| `forget` | Explicit delete OR auto-sweep based on `forgettingRisk`. Pinned (`importance>=1.0`) and caveat-layer memories are always preserved. **v0.3.0** `interactive` flag asks the user via Elicitation before deleting a specific memory_id. |
|
|
172
|
+
| `consolidate` | Sleep-mode compression: cluster cold low-importance memories → protected learning-layer summary. Supports `dry_run` preview. **v0.3.0** `use_llm` flag asks the client LLM (Sampling) to rewrite cluster summaries into prose. |
|
|
162
173
|
|
|
163
174
|
### CLI utilities
|
|
164
175
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
interface ElicitResult {
|
|
3
|
+
action: 'accept' | 'decline' | 'cancel' | 'unsupported';
|
|
4
|
+
content?: Record<string, unknown>;
|
|
5
|
+
reason?: string;
|
|
6
|
+
}
|
|
7
|
+
interface ElicitParams {
|
|
8
|
+
message: string;
|
|
9
|
+
requestedSchema: {
|
|
10
|
+
type: 'object';
|
|
11
|
+
properties: Record<string, unknown>;
|
|
12
|
+
required?: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare function elicit(server: Server, p: ElicitParams): Promise<ElicitResult>;
|
|
16
|
+
export declare function confirmForget(server: Server, candidate: {
|
|
17
|
+
id: number;
|
|
18
|
+
entity: string;
|
|
19
|
+
layer: string;
|
|
20
|
+
importance: number;
|
|
21
|
+
preview: string;
|
|
22
|
+
}): Promise<boolean>;
|
|
23
|
+
export declare function confirmPin(server: Server, memoryId: number, preview: string, newImportance: number): Promise<boolean>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Elicitation block — server asks the client (and ultimately the user) a structured question.
|
|
2
|
+
//
|
|
3
|
+
// MCP semantics: server.request({method: 'elicitation/create', params: {message, requestedSchema}})
|
|
4
|
+
// The user responds via the client UI. Used by:
|
|
5
|
+
// - Stale-memory cleanup (forget candidates require confirmation)
|
|
6
|
+
// - Pin/unpin confirmation when importance crosses 0.9
|
|
7
|
+
//
|
|
8
|
+
// Clients without elicitation support fail gracefully — caller falls back to "decline = skip".
|
|
9
|
+
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
export async function elicit(server, p) {
|
|
11
|
+
try {
|
|
12
|
+
const res = await server.request({ method: 'elicitation/create', params: p }, ElicitRequestSchema);
|
|
13
|
+
return {
|
|
14
|
+
action: res?.action ?? 'decline',
|
|
15
|
+
content: res?.content,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return { action: 'unsupported', reason: err?.message ?? String(err) };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function confirmForget(server, candidate) {
|
|
23
|
+
const res = await elicit(server, {
|
|
24
|
+
message: `Forget memory #${candidate.id} for "${candidate.entity}"?\n\nLayer: ${candidate.layer} Importance: ${candidate.importance.toFixed(2)}\nPreview: ${candidate.preview.slice(0, 200)}`,
|
|
25
|
+
requestedSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
confirm: {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
title: 'Forget this memory',
|
|
31
|
+
description: 'Yes = delete permanently. No = keep.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['confirm'],
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (res.action === 'accept' && res.content && typeof res.content.confirm === 'boolean') {
|
|
38
|
+
return res.content.confirm;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
export async function confirmPin(server, memoryId, preview, newImportance) {
|
|
43
|
+
const res = await elicit(server, {
|
|
44
|
+
message: `Pin memory #${memoryId}? (importance ${newImportance.toFixed(2)})\n\nPreview: ${preview.slice(0, 200)}\n\nPinned memories survive forget-sweeps and consolidation.`,
|
|
45
|
+
requestedSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
confirm: { type: 'boolean', title: 'Pin this memory', description: 'Yes = pin. No = save without pinning.' },
|
|
49
|
+
},
|
|
50
|
+
required: ['confirm'],
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (res.action === 'accept' && res.content && typeof res.content.confirm === 'boolean') {
|
|
54
|
+
return res.content.confirm;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=elicitation.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const PROMPTS: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
arguments: {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
required: boolean;
|
|
8
|
+
}[];
|
|
9
|
+
}[];
|
|
10
|
+
interface PromptMessage {
|
|
11
|
+
role: 'user' | 'assistant';
|
|
12
|
+
content: {
|
|
13
|
+
type: 'text';
|
|
14
|
+
text: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function getPrompt(name: string, args: Record<string, string> | undefined): {
|
|
18
|
+
description?: string;
|
|
19
|
+
messages: PromptMessage[];
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Prompts block — reusable prompt templates that agents can pull from the server.
|
|
2
|
+
//
|
|
3
|
+
// Templates:
|
|
4
|
+
// summarize-session — turn a chat transcript into structured memories (1 per layer)
|
|
5
|
+
// extract-caveats — read text and produce caveat-layer entries (pain lessons)
|
|
6
|
+
// weekly-consolidation — sleep-mode summary of the past week's memories
|
|
7
|
+
// recall-and-write — recall first, then write — anti-pattern guard
|
|
8
|
+
// entity-handoff — produce a handoff doc for an entity (name + memories + next steps)
|
|
9
|
+
//
|
|
10
|
+
// Each prompt accepts arguments and returns a list of messages the client can feed to its LLM.
|
|
11
|
+
export const PROMPTS = [
|
|
12
|
+
{
|
|
13
|
+
name: 'summarize-session',
|
|
14
|
+
description: 'Turn a chat session transcript into structured memories. Produces up to 6 memories (one per layer) capturing goal/context/emotion/implementation/caveat/learning. Use at session end.',
|
|
15
|
+
arguments: [
|
|
16
|
+
{ name: 'transcript', description: 'The session transcript text. Free-form.', required: true },
|
|
17
|
+
{ name: 'entity_hint', description: 'Optional canonical entity name to attach the memories to.', required: false },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'extract-caveats',
|
|
22
|
+
description: 'Scan a body of text (post-mortem, error log, decision doc) and propose caveat-layer memories — concise pain lessons starting with verbs ("Never", "Always", "Watch out"). Returns JSON list of caveats.',
|
|
23
|
+
arguments: [
|
|
24
|
+
{ name: 'text', description: 'Source text (post-mortem, debug session, retro). Free-form.', required: true },
|
|
25
|
+
{ name: 'entity_hint', description: 'Optional canonical entity name for the caveats.', required: false },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'weekly-consolidation',
|
|
30
|
+
description: 'Sleep-mode summary of the past week\'s memories for an entity. Produces a single learning-layer entry that captures the trajectory. Use as input to the consolidate tool, or to write a Friday digest.',
|
|
31
|
+
arguments: [
|
|
32
|
+
{ name: 'entity_name', description: 'The entity to consolidate.', required: true },
|
|
33
|
+
{ name: 'week_offset', description: 'Weeks-ago offset (0 = this week, 1 = last week). Default 0.', required: false },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'recall-and-write',
|
|
38
|
+
description: 'Anti-pattern guard. Before writing code, draft a doc, or making a decision: recall relevant memories first, then produce the answer with explicit citations to the recalled memory_ids. Forces "memory before action" discipline.',
|
|
39
|
+
arguments: [
|
|
40
|
+
{ name: 'task', description: 'What you are about to do (code task, decision, doc draft). One sentence.', required: true },
|
|
41
|
+
{ name: 'entity_hint', description: 'Optional entity to focus recall on.', required: false },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'entity-handoff',
|
|
46
|
+
description: 'Produce a handoff document for an entity: name, kind, key memories per layer, current open questions, and next steps. Use when transferring context to a new session, a new agent, or a new collaborator.',
|
|
47
|
+
arguments: [
|
|
48
|
+
{ name: 'entity_name', description: 'The entity to hand off.', required: true },
|
|
49
|
+
{ name: 'audience', description: 'Who receives the handoff (e.g. "new claude session", "human teammate"). Default "new claude session".', required: false },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
export function getPrompt(name, args) {
|
|
54
|
+
const a = args ?? {};
|
|
55
|
+
switch (name) {
|
|
56
|
+
case 'summarize-session': {
|
|
57
|
+
const transcript = a.transcript ?? '';
|
|
58
|
+
const entityHint = a.entity_hint ? `\n\nFocus entity: ${a.entity_hint}` : '';
|
|
59
|
+
return {
|
|
60
|
+
description: 'Summarize the session into 6-layer structured memories.',
|
|
61
|
+
messages: [
|
|
62
|
+
{
|
|
63
|
+
role: 'user',
|
|
64
|
+
content: {
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: `You are an agent-memory writer. Read the session transcript below and propose memories to save. Output a JSON array; each item has {entity_name, entity_kind, layer, content, importance}.
|
|
67
|
+
|
|
68
|
+
Layers (use exactly one per memory):
|
|
69
|
+
- goal: WHY this work exists, target outcome
|
|
70
|
+
- context: WHY THIS NOW, situation, timing
|
|
71
|
+
- emotion: USER tone, feelings expressed
|
|
72
|
+
- implementation: HOW it was done, what worked, what failed
|
|
73
|
+
- caveat: PAIN lesson, "never X" / "always Y" — these are protected from forgetting
|
|
74
|
+
- learning: GROWTH, decisions made, insights
|
|
75
|
+
|
|
76
|
+
Rules:
|
|
77
|
+
- Max 6 entries (one per layer). Skip layers with nothing worth saving.
|
|
78
|
+
- Importance 0-1. Set 0.9+ to pin (use sparingly).
|
|
79
|
+
- Caveats must be 1 sentence and start with a verb.
|
|
80
|
+
- Quote nothing verbatim; summarize.
|
|
81
|
+
|
|
82
|
+
Transcript:
|
|
83
|
+
${transcript}${entityHint}`,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
case 'extract-caveats': {
|
|
90
|
+
const text = a.text ?? '';
|
|
91
|
+
const entityHint = a.entity_hint ? `\n\nFocus entity: ${a.entity_hint}` : '';
|
|
92
|
+
return {
|
|
93
|
+
description: 'Extract caveat-layer pain lessons.',
|
|
94
|
+
messages: [
|
|
95
|
+
{
|
|
96
|
+
role: 'user',
|
|
97
|
+
content: {
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: `You are a caveat extractor. Read the source text below and output a JSON list of caveats. Each caveat:
|
|
100
|
+
- Starts with a verb ("Never", "Always", "Watch out", "Reject", "Confirm")
|
|
101
|
+
- Is one sentence
|
|
102
|
+
- Is concrete (a specific failure mode, not abstract advice)
|
|
103
|
+
- Captures something the reader does NOT want to relearn the hard way
|
|
104
|
+
|
|
105
|
+
Output format: [{"content": "Never X when Y, because Z", "importance": 0.7-1.0}]
|
|
106
|
+
|
|
107
|
+
Source text:
|
|
108
|
+
${text}${entityHint}`,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
case 'weekly-consolidation': {
|
|
115
|
+
const entityName = a.entity_name ?? '<entity>';
|
|
116
|
+
const weekOffset = a.week_offset ?? '0';
|
|
117
|
+
return {
|
|
118
|
+
description: `Consolidate the last week of memories for ${entityName}.`,
|
|
119
|
+
messages: [
|
|
120
|
+
{
|
|
121
|
+
role: 'user',
|
|
122
|
+
content: {
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: `Consolidate the past week's memories for entity "${entityName}" (week_offset=${weekOffset}).
|
|
125
|
+
|
|
126
|
+
Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=4000) to retrieve recent memories.
|
|
127
|
+
Step 2: Read the returned memories and produce ONE learning-layer summary that captures:
|
|
128
|
+
- What we set out to do (goal trajectory)
|
|
129
|
+
- What actually happened (implementation summary)
|
|
130
|
+
- What we learned (1-3 insights)
|
|
131
|
+
- Any caveats we should never forget (preserve these — do NOT consolidate them away)
|
|
132
|
+
|
|
133
|
+
Step 3: Output a JSON object {entity_name, layer:"learning", content, importance:0.7}.
|
|
134
|
+
|
|
135
|
+
Do not write to memory directly — just return the JSON. The user will choose whether to save.`,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
case 'recall-and-write': {
|
|
142
|
+
const task = a.task ?? '<task>';
|
|
143
|
+
const entityHint = a.entity_hint ?? '';
|
|
144
|
+
const recallCmd = entityHint
|
|
145
|
+
? `recall(query="${task}", entity_name="${entityHint}", max_tokens=2000)`
|
|
146
|
+
: `recall(query="${task}", max_tokens=2000)`;
|
|
147
|
+
return {
|
|
148
|
+
description: 'Memory-before-action discipline.',
|
|
149
|
+
messages: [
|
|
150
|
+
{
|
|
151
|
+
role: 'user',
|
|
152
|
+
content: {
|
|
153
|
+
type: 'text',
|
|
154
|
+
text: `Before doing this task, recall first.
|
|
155
|
+
|
|
156
|
+
Task: ${task}
|
|
157
|
+
|
|
158
|
+
Step 1: Call ${recallCmd}.
|
|
159
|
+
Step 2: Skim the returned memories. Identify any caveats that apply.
|
|
160
|
+
Step 3: Produce your output with INLINE citations to relevant memory_ids: e.g. "Use better-sqlite3 v12+ [memory:1234] because v11 breaks on Node 24 [memory:5678]."
|
|
161
|
+
Step 4: If you found NO relevant memories, say so explicitly: "No prior memories on this — proceeding from first principles."
|
|
162
|
+
|
|
163
|
+
Goal: never solve a problem twice without checking.`,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case 'entity-handoff': {
|
|
170
|
+
const entityName = a.entity_name ?? '<entity>';
|
|
171
|
+
const audience = a.audience ?? 'new claude session';
|
|
172
|
+
return {
|
|
173
|
+
description: `Produce a handoff document for ${entityName}.`,
|
|
174
|
+
messages: [
|
|
175
|
+
{
|
|
176
|
+
role: 'user',
|
|
177
|
+
content: {
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: `Produce a handoff document for entity "${entityName}", aimed at: ${audience}.
|
|
180
|
+
|
|
181
|
+
Step 1: Call recall(query="${entityName}", entity_name="${entityName}", max_tokens=6000).
|
|
182
|
+
Step 2: Skim memories grouped by layer.
|
|
183
|
+
Step 3: Output a markdown doc with these sections:
|
|
184
|
+
- **Identity**: name, kind, canonical key (if any)
|
|
185
|
+
- **Goal** (from goal-layer memories): what this entity is for
|
|
186
|
+
- **State** (from latest implementation memories): where things stand right now
|
|
187
|
+
- **Caveats** (from caveat-layer memories): what NEVER to do, with memory_id citations
|
|
188
|
+
- **Open questions**: things the prior session left unresolved
|
|
189
|
+
- **Suggested next steps**: 3 concrete actions for the audience
|
|
190
|
+
|
|
191
|
+
Keep it under 1 page. Cite memory_ids inline like [memory:1234].`,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
throw new Error(`unknown prompt: ${name}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
export declare const STATIC_RESOURCES: {
|
|
3
|
+
uri: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
}[];
|
|
8
|
+
export declare const RESOURCE_TEMPLATES: {
|
|
9
|
+
uriTemplate: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
}[];
|
|
14
|
+
export declare function readResource(db: Database.Database, uri: string): {
|
|
15
|
+
uri: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
text: string;
|
|
18
|
+
};
|
|
@@ -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
|
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) {
|
|
@@ -494,8 +508,8 @@ function handleForget(args) {
|
|
|
494
508
|
}
|
|
495
509
|
// Auto-sweep — also respect pin (importance >= 0.9) as protection
|
|
496
510
|
const rows = db
|
|
497
|
-
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected
|
|
498
|
-
FROM memories
|
|
511
|
+
.prepare(`SELECT id, layer, importance, access_count, last_accessed_at, protected
|
|
512
|
+
FROM memories
|
|
499
513
|
WHERE protected = 0 AND importance < 0.9`)
|
|
500
514
|
.all();
|
|
501
515
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -543,17 +557,17 @@ function handleConsolidate(args) {
|
|
|
543
557
|
// read-only audit: count candidates using the same rules.
|
|
544
558
|
const now = Math.floor(Date.now() / 1000);
|
|
545
559
|
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
|
|
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
|
|
557
571
|
`).all(ageCutoff);
|
|
558
572
|
const totalReplaced = candidates.reduce((s, c) => s + c.c, 0);
|
|
559
573
|
return JSON.stringify({
|
|
@@ -627,19 +641,19 @@ function handleListEntities(args) {
|
|
|
627
641
|
const minMemories = Math.max(1, Number(args?.min_memories ?? 1));
|
|
628
642
|
const limit = Math.max(1, Math.min(200, Number(args?.limit ?? 30)));
|
|
629
643
|
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
|
|
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
|
|
643
657
|
`;
|
|
644
658
|
const params = [];
|
|
645
659
|
if (kind) {
|
|
@@ -691,29 +705,29 @@ function handleRecallFile(args) {
|
|
|
691
705
|
return JSON.stringify({ ok: true, count: 0, note: 'No edits found for that path substring.' });
|
|
692
706
|
}
|
|
693
707
|
// 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
|
|
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
|
|
698
712
|
`).all(`%${sub}%`);
|
|
699
713
|
// 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 ?
|
|
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 ?
|
|
707
721
|
`).all(`%${sub}%`, maxIntents);
|
|
708
722
|
// 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
|
|
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
|
|
717
731
|
`).all(`%${sub}%`);
|
|
718
732
|
// Distinct file paths matched (the substring may match multiple files)
|
|
719
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}%`);
|
|
@@ -746,6 +760,158 @@ function handleReadSmart(args) {
|
|
|
746
760
|
return handleReadSmartImpl(db, { path: args.path, force: args.force });
|
|
747
761
|
}
|
|
748
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
|
+
// ============================================================
|
|
749
915
|
// MCP wiring
|
|
750
916
|
// ============================================================
|
|
751
917
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
@@ -767,13 +933,13 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
767
933
|
text = handleListEntities(args);
|
|
768
934
|
break;
|
|
769
935
|
case 'forget':
|
|
770
|
-
text =
|
|
936
|
+
text = await handleForgetInteractive(args);
|
|
771
937
|
break;
|
|
772
938
|
case 'consolidate':
|
|
773
|
-
text =
|
|
939
|
+
text = await handleConsolidateWithSampling(args);
|
|
774
940
|
break;
|
|
775
941
|
case 'recall_file':
|
|
776
|
-
text =
|
|
942
|
+
text = await handleRecallFileWithRoots(args);
|
|
777
943
|
break;
|
|
778
944
|
case 'read_smart':
|
|
779
945
|
text = handleReadSmart(args);
|
|
@@ -789,6 +955,24 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
789
955
|
};
|
|
790
956
|
}
|
|
791
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
|
+
});
|
|
792
976
|
const transport = new StdioServerTransport();
|
|
793
977
|
await server.connect(transport);
|
|
794
978
|
process.stderr.write(`[linksee-memory] MCP server ready on stdio (v${SERVER_VERSION})\n`);
|
package/package.json
CHANGED
|
@@ -1,71 +1,82 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "linksee-memory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"mcpName": "io.github.michielinksee/linksee-memory",
|
|
5
|
-
"description": "Local-first agent memory MCP — cross-agent brain with 6-layer structured memory + token-saving file diff cache",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"linksee-memory": "dist/mcp/server.js",
|
|
9
|
-
"linksee-memory-import": "dist/bin/import-sessions.js",
|
|
10
|
-
"linksee-memory-sync": "dist/bin/sync-session.js",
|
|
11
|
-
"linksee-memory-install-skill": "dist/bin/install-skill.js",
|
|
12
|
-
"linksee-memory-stats": "dist/bin/stats.js"
|
|
13
|
-
},
|
|
14
|
-
"main": "./dist/mcp/server.js",
|
|
15
|
-
"files": [
|
|
16
|
-
"dist/**/*.js",
|
|
17
|
-
"dist/**/*.d.ts",
|
|
18
|
-
"dist/db/schema.sql",
|
|
19
|
-
"dist/skill/SKILL.md",
|
|
20
|
-
"README.md",
|
|
21
|
-
"LICENSE"
|
|
22
|
-
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"build": "tsc && node -e \"require('fs').copyFileSync('src/db/schema.sql','dist/db/schema.sql'); require('fs').mkdirSync('dist/skill',{recursive:true}); require('fs').copyFileSync('src/skill/SKILL.md','dist/skill/SKILL.md')\"",
|
|
25
|
-
"start": "node dist/mcp/server.js",
|
|
26
|
-
"dev": "tsx src/mcp/server.ts",
|
|
27
|
-
"migrate": "node dist/db/migrate.js",
|
|
28
|
-
"migrate:dev": "tsx src/db/migrate.ts",
|
|
29
|
-
"prepublishOnly": "npm run build"
|
|
30
|
-
},
|
|
31
|
-
"keywords": [
|
|
32
|
-
"mcp",
|
|
33
|
-
"model-context-protocol",
|
|
34
|
-
"memory",
|
|
35
|
-
"agent",
|
|
36
|
-
"agent-memory",
|
|
37
|
-
"claude",
|
|
38
|
-
"claude-code",
|
|
39
|
-
"cursor",
|
|
40
|
-
"chatgpt",
|
|
41
|
-
"sqlite",
|
|
42
|
-
"local-first",
|
|
43
|
-
"token-savings",
|
|
44
|
-
"cross-agent"
|
|
45
|
-
],
|
|
46
|
-
"author": "Synapse Arrows PTE. LTD.",
|
|
47
|
-
"license": "MIT",
|
|
48
|
-
"homepage": "https://github.com/michielinksee/linksee-memory",
|
|
49
|
-
"repository": {
|
|
50
|
-
"type": "git",
|
|
51
|
-
"url": "git+https://github.com/michielinksee/linksee-memory.git"
|
|
52
|
-
},
|
|
53
|
-
"bugs": {
|
|
54
|
-
"url": "https://github.com/michielinksee/linksee-memory/issues"
|
|
55
|
-
},
|
|
56
|
-
"dependencies": {
|
|
57
|
-
"@babel/parser": "^7.25.0",
|
|
58
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
59
|
-
"better-sqlite3": "^
|
|
60
|
-
"sqlite-vec": "^0.1.6"
|
|
61
|
-
},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"@types/better-sqlite3": "^7.6.11",
|
|
64
|
-
"@types/node": "^22.7.0",
|
|
65
|
-
"tsx": "^4.19.0",
|
|
66
|
-
"typescript": "^5.6.0"
|
|
67
|
-
},
|
|
68
|
-
"engines": {
|
|
69
|
-
"node": ">=20"
|
|
70
|
-
}
|
|
71
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "linksee-memory",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"mcpName": "io.github.michielinksee/linksee-memory",
|
|
5
|
+
"description": "Local-first agent memory MCP — cross-agent brain with 6-layer structured memory + token-saving file diff cache",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"linksee-memory": "dist/mcp/server.js",
|
|
9
|
+
"linksee-memory-import": "dist/bin/import-sessions.js",
|
|
10
|
+
"linksee-memory-sync": "dist/bin/sync-session.js",
|
|
11
|
+
"linksee-memory-install-skill": "dist/bin/install-skill.js",
|
|
12
|
+
"linksee-memory-stats": "dist/bin/stats.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/mcp/server.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/**/*.js",
|
|
17
|
+
"dist/**/*.d.ts",
|
|
18
|
+
"dist/db/schema.sql",
|
|
19
|
+
"dist/skill/SKILL.md",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && node -e \"require('fs').copyFileSync('src/db/schema.sql','dist/db/schema.sql'); require('fs').mkdirSync('dist/skill',{recursive:true}); require('fs').copyFileSync('src/skill/SKILL.md','dist/skill/SKILL.md')\"",
|
|
25
|
+
"start": "node dist/mcp/server.js",
|
|
26
|
+
"dev": "tsx src/mcp/server.ts",
|
|
27
|
+
"migrate": "node dist/db/migrate.js",
|
|
28
|
+
"migrate:dev": "tsx src/db/migrate.ts",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"model-context-protocol",
|
|
34
|
+
"memory",
|
|
35
|
+
"agent",
|
|
36
|
+
"agent-memory",
|
|
37
|
+
"claude",
|
|
38
|
+
"claude-code",
|
|
39
|
+
"cursor",
|
|
40
|
+
"chatgpt",
|
|
41
|
+
"sqlite",
|
|
42
|
+
"local-first",
|
|
43
|
+
"token-savings",
|
|
44
|
+
"cross-agent"
|
|
45
|
+
],
|
|
46
|
+
"author": "Synapse Arrows PTE. LTD.",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"homepage": "https://github.com/michielinksee/linksee-memory",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/michielinksee/linksee-memory.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/michielinksee/linksee-memory/issues"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@babel/parser": "^7.25.0",
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
59
|
+
"better-sqlite3": "^12.9.0",
|
|
60
|
+
"sqlite-vec": "^0.1.6"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
64
|
+
"@types/node": "^22.7.0",
|
|
65
|
+
"tsx": "^4.19.0",
|
|
66
|
+
"typescript": "^5.6.0"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=20"
|
|
70
|
+
},
|
|
71
|
+
"overrides": {
|
|
72
|
+
"ip-address": "^10.2.0"
|
|
73
|
+
},
|
|
74
|
+
"pnpm": {
|
|
75
|
+
"onlyBuiltDependencies": [
|
|
76
|
+
"better-sqlite3"
|
|
77
|
+
],
|
|
78
|
+
"overrides": {
|
|
79
|
+
"ip-address": "^10.2.0"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|