@adamrdrew/agent-memory-mcp 1.0.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/LICENSE +674 -0
- package/README.md +109 -0
- package/dist/embedder.d.ts +11 -0
- package/dist/embedder.js +38 -0
- package/dist/embedder.js.map +1 -0
- package/dist/hardcopy-store.d.ts +17 -0
- package/dist/hardcopy-store.js +80 -0
- package/dist/hardcopy-store.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-store.d.ts +33 -0
- package/dist/memory-store.js +310 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +11 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +54 -0
- package/dist/tools.js +192 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# agent-memory-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for persistent agent memory, backed by [LanceDB](https://lancedb.com/) with hybrid BM25 + vector search. Gives AI agents the ability to store, search, and manage memories across sessions using the [Model Context Protocol](https://modelcontextprotocol.io/).
|
|
4
|
+
|
|
5
|
+
All data stays on your machine. Embeddings are generated locally using [all-MiniLM-L6-v2](https://huggingface.co/Xenova/all-MiniLM-L6-v2) via ONNX — no API keys, no network dependencies after initial setup.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Hybrid search** — combines BM25 full-text search with cosine vector similarity via Reciprocal Rank Fusion (RRF)
|
|
10
|
+
- **Local embeddings** — runs Xenova/all-MiniLM-L6-v2 locally via ONNX, no external API calls
|
|
11
|
+
- **12 memory categories** — structured taxonomy for organising memories
|
|
12
|
+
- **Batch operations** — store multiple memories in a single call
|
|
13
|
+
- **Hardcopy backup** — optional JSON file mirror of all mutations for human-readable backup
|
|
14
|
+
- **Fully local** — all data stays on disk, no network dependencies after first model download
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Install the package globally first. This downloads the embedding model (~80 MB) so it's ready when the server starts:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @adamrdrew/agent-memory-mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then add the server to your MCP client configuration.
|
|
25
|
+
|
|
26
|
+
### Claude Desktop
|
|
27
|
+
|
|
28
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"agent-memory": {
|
|
34
|
+
"command": "agent-memory-mcp",
|
|
35
|
+
"env": {
|
|
36
|
+
"MEMORY_DB_PATH": "/path/to/your/memory-db"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Claude Code
|
|
44
|
+
|
|
45
|
+
Add to your project's `.mcp.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"agent-memory": {
|
|
50
|
+
"command": "agent-memory-mcp",
|
|
51
|
+
"env": {
|
|
52
|
+
"MEMORY_DB_PATH": "/path/to/your/memory-db"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
| Variable | Required | Description |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `MEMORY_DB_PATH` | Yes | Path to the LanceDB database directory |
|
|
63
|
+
| `EMBEDDING_MODEL` | No | HuggingFace model ID (default: `Xenova/all-MiniLM-L6-v2`) |
|
|
64
|
+
| `ENABLE_HARDCOPY` | No | Set to `true` to enable JSON file backup |
|
|
65
|
+
| `HARDCOPY_PATH` | If hardcopy enabled | Directory for JSON mirror files |
|
|
66
|
+
|
|
67
|
+
## Tools
|
|
68
|
+
|
|
69
|
+
| Tool | Description |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `store` | Store a single memory with content, category, and tags |
|
|
72
|
+
| `store_batch` | Store multiple memories in one call |
|
|
73
|
+
| `search` | Search memories by meaning and/or keywords. Supports hybrid, keyword, and semantic modes |
|
|
74
|
+
| `recall` | Multi-topic contextual recall — searches multiple topics in parallel and includes recent memories |
|
|
75
|
+
| `find_related` | Find memories similar to a specific memory |
|
|
76
|
+
| `list_recent` | List most recent memories, optionally filtered by category |
|
|
77
|
+
| `update` | Update an existing memory — re-embeds automatically if content changes |
|
|
78
|
+
| `delete` | Permanently remove a memory by ID |
|
|
79
|
+
| `stats` | Get database statistics: total count, breakdown by category, timestamps |
|
|
80
|
+
|
|
81
|
+
## Search Modes
|
|
82
|
+
|
|
83
|
+
The `search` tool supports three modes:
|
|
84
|
+
|
|
85
|
+
- **`hybrid`** (default) — combines BM25 keyword scoring with vector similarity using RRF reranking. Falls back to semantic-only if the full-text index is unavailable.
|
|
86
|
+
- **`keyword`** — BM25 full-text search only.
|
|
87
|
+
- **`semantic`** — cosine vector similarity only.
|
|
88
|
+
|
|
89
|
+
All modes support filtering by category, tags, and date range.
|
|
90
|
+
|
|
91
|
+
## Memory Categories
|
|
92
|
+
|
|
93
|
+
`code-solution` · `bug-fix` · `architecture` · `learning` · `tool-usage` · `debugging` · `performance` · `security` · `observation` · `personal` · `relationship` · `other`
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/adamrdrew/agent-memory-mcp.git
|
|
99
|
+
cd agent-memory-mcp
|
|
100
|
+
npm install
|
|
101
|
+
npm run dev # Run with tsx (no build step)
|
|
102
|
+
npm run build # Compile TypeScript to dist/
|
|
103
|
+
npm test # Run all tests
|
|
104
|
+
npm run test:watch # Run tests in watch mode
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
This project is licensed under the [GNU General Public License v3.0](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Embedder } from './types.js';
|
|
2
|
+
export declare class TransformersEmbedder implements Embedder {
|
|
3
|
+
private extractor;
|
|
4
|
+
private readonly modelName;
|
|
5
|
+
constructor(modelName?: string);
|
|
6
|
+
initialize(): Promise<void>;
|
|
7
|
+
embed(text: string): Promise<number[]>;
|
|
8
|
+
embedBatch(texts: string[]): Promise<number[][]>;
|
|
9
|
+
dimensions(): number;
|
|
10
|
+
private requireExtractor;
|
|
11
|
+
}
|
package/dist/embedder.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const EMBEDDING_DIMENSIONS = 384;
|
|
2
|
+
export class TransformersEmbedder {
|
|
3
|
+
extractor = null;
|
|
4
|
+
modelName;
|
|
5
|
+
constructor(modelName = 'Xenova/all-MiniLM-L6-v2') {
|
|
6
|
+
this.modelName = modelName;
|
|
7
|
+
}
|
|
8
|
+
async initialize() {
|
|
9
|
+
const { pipeline } = await import('@xenova/transformers');
|
|
10
|
+
this.extractor = await pipeline('feature-extraction', this.modelName);
|
|
11
|
+
}
|
|
12
|
+
async embed(text) {
|
|
13
|
+
const extractor = this.requireExtractor();
|
|
14
|
+
const output = await extractor(text, { pooling: 'mean', normalize: true });
|
|
15
|
+
return Array.from(output.data);
|
|
16
|
+
}
|
|
17
|
+
async embedBatch(texts) {
|
|
18
|
+
const extractor = this.requireExtractor();
|
|
19
|
+
const output = await extractor(texts, { pooling: 'mean', normalize: true });
|
|
20
|
+
const data = output.data;
|
|
21
|
+
const dim = this.dimensions();
|
|
22
|
+
const vectors = [];
|
|
23
|
+
for (let i = 0; i < texts.length; i++) {
|
|
24
|
+
vectors.push(Array.from(data.slice(i * dim, (i + 1) * dim)));
|
|
25
|
+
}
|
|
26
|
+
return vectors;
|
|
27
|
+
}
|
|
28
|
+
dimensions() {
|
|
29
|
+
return EMBEDDING_DIMENSIONS;
|
|
30
|
+
}
|
|
31
|
+
requireExtractor() {
|
|
32
|
+
if (!this.extractor) {
|
|
33
|
+
throw new Error('Embedder not initialised. Call initialize() first.');
|
|
34
|
+
}
|
|
35
|
+
return this.extractor;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=embedder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedder.js","sourceRoot":"","sources":["../src/embedder.ts"],"names":[],"mappings":"AAEA,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAEjC,MAAM,OAAO,oBAAoB;IACvB,SAAS,GAA6B,IAAI,CAAC;IAClC,SAAS,CAAS;IAEnC,YAAY,YAAoB,yBAAyB;QACvD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAC1D,IAAI,CAAC,SAAS,GAAG,MAAM,QAAQ,CAAC,oBAAoB,EAAE,IAAI,CAAC,SAAS,CAAsB,CAAC;IAC7F,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3E,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAoB,CAAC,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,KAAe;QAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5E,MAAM,IAAI,GAAG,MAAM,CAAC,IAAoB,CAAC;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAe,EAAE,CAAC;QAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,UAAU;QACR,OAAO,oBAAoB,CAAC;IAC9B,CAAC;IAEO,gBAAgB;QACtB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACxE,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;CACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Memory, MemoryCategory, MemoryStore, MemoryStats, SearchFilters, SearchMode, SearchResult, StoreRequest, UpdateRequest } from './types.js';
|
|
2
|
+
export declare class HardcopyMemoryStore implements MemoryStore {
|
|
3
|
+
private readonly inner;
|
|
4
|
+
private readonly hardcopyPath;
|
|
5
|
+
constructor(inner: MemoryStore, hardcopyPath: string);
|
|
6
|
+
initialize(): Promise<void>;
|
|
7
|
+
store(request: StoreRequest): Promise<Memory>;
|
|
8
|
+
storeBatch(requests: StoreRequest[]): Promise<Memory[]>;
|
|
9
|
+
update(id: string, updates: UpdateRequest): Promise<Memory>;
|
|
10
|
+
delete(id: string): Promise<void>;
|
|
11
|
+
search(query: string, mode: SearchMode, filters: SearchFilters): Promise<SearchResult[]>;
|
|
12
|
+
findRelated(memoryId: string, limit: number): Promise<SearchResult[]>;
|
|
13
|
+
listRecent(limit: number, category?: MemoryCategory): Promise<Memory[]>;
|
|
14
|
+
stats(): Promise<MemoryStats>;
|
|
15
|
+
private writeHardcopy;
|
|
16
|
+
private deleteHardcopy;
|
|
17
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { mkdir, writeFile, unlink } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
// ── HardcopyMemoryStore ─────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Transparent decorator that mirrors all mutations to plain JSON
|
|
6
|
+
// files on disk. One file per memory, named {id}.json. Read
|
|
7
|
+
// operations delegate straight through — the hardcopy is write-only.
|
|
8
|
+
//
|
|
9
|
+
// Hardcopy errors are logged to stderr but never propagate.
|
|
10
|
+
// The primary store is the source of truth; the hardcopy is
|
|
11
|
+
// a human-readable escape hatch.
|
|
12
|
+
export class HardcopyMemoryStore {
|
|
13
|
+
inner;
|
|
14
|
+
hardcopyPath;
|
|
15
|
+
constructor(inner, hardcopyPath) {
|
|
16
|
+
this.inner = inner;
|
|
17
|
+
this.hardcopyPath = hardcopyPath;
|
|
18
|
+
}
|
|
19
|
+
async initialize() {
|
|
20
|
+
await this.inner.initialize();
|
|
21
|
+
await mkdir(this.hardcopyPath, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
// ── Mutations (mirrored to disk) ────────────────────────────
|
|
24
|
+
async store(request) {
|
|
25
|
+
const memory = await this.inner.store(request);
|
|
26
|
+
await this.writeHardcopy(memory);
|
|
27
|
+
return memory;
|
|
28
|
+
}
|
|
29
|
+
async storeBatch(requests) {
|
|
30
|
+
const memories = await this.inner.storeBatch(requests);
|
|
31
|
+
await Promise.all(memories.map(m => this.writeHardcopy(m)));
|
|
32
|
+
return memories;
|
|
33
|
+
}
|
|
34
|
+
async update(id, updates) {
|
|
35
|
+
const memory = await this.inner.update(id, updates);
|
|
36
|
+
await this.writeHardcopy(memory);
|
|
37
|
+
return memory;
|
|
38
|
+
}
|
|
39
|
+
async delete(id) {
|
|
40
|
+
await this.inner.delete(id);
|
|
41
|
+
await this.deleteHardcopy(id);
|
|
42
|
+
}
|
|
43
|
+
// ── Reads (pass through) ────────────────────────────────────
|
|
44
|
+
async search(query, mode, filters) {
|
|
45
|
+
return this.inner.search(query, mode, filters);
|
|
46
|
+
}
|
|
47
|
+
async findRelated(memoryId, limit) {
|
|
48
|
+
return this.inner.findRelated(memoryId, limit);
|
|
49
|
+
}
|
|
50
|
+
async listRecent(limit, category) {
|
|
51
|
+
return this.inner.listRecent(limit, category);
|
|
52
|
+
}
|
|
53
|
+
async stats() {
|
|
54
|
+
return this.inner.stats();
|
|
55
|
+
}
|
|
56
|
+
// ── Private ─────────────────────────────────────────────────
|
|
57
|
+
async writeHardcopy(memory) {
|
|
58
|
+
try {
|
|
59
|
+
const filePath = join(this.hardcopyPath, `${memory.id}.json`);
|
|
60
|
+
await writeFile(filePath, JSON.stringify(memory, null, 2) + '\n');
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(`[hardcopy] Failed to write ${memory.id}:`, err);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async deleteHardcopy(id) {
|
|
67
|
+
try {
|
|
68
|
+
const filePath = join(this.hardcopyPath, `${id}.json`);
|
|
69
|
+
await unlink(filePath);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// File may not exist (e.g. hardcopy was enabled after the memory
|
|
73
|
+
// was created). That's fine — don't log ENOENT noise.
|
|
74
|
+
if (err.code !== 'ENOENT') {
|
|
75
|
+
console.error(`[hardcopy] Failed to delete ${id}:`, err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=hardcopy-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hardcopy-store.js","sourceRoot":"","sources":["../src/hardcopy-store.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { TransformersEmbedder } from './embedder.js';
|
|
4
|
+
import { HardcopyMemoryStore } from './hardcopy-store.js';
|
|
5
|
+
import { LanceMemoryStore } from './memory-store.js';
|
|
6
|
+
import { createServer } from './server.js';
|
|
7
|
+
async function main() {
|
|
8
|
+
const dbPath = process.env.MEMORY_DB_PATH;
|
|
9
|
+
if (!dbPath) {
|
|
10
|
+
console.error('MEMORY_DB_PATH environment variable is required');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const modelName = process.env.EMBEDDING_MODEL ?? 'Xenova/all-MiniLM-L6-v2';
|
|
14
|
+
// ── Compose dependencies ──
|
|
15
|
+
const embedder = new TransformersEmbedder(modelName);
|
|
16
|
+
let store = new LanceMemoryStore(dbPath, embedder);
|
|
17
|
+
if (process.env.ENABLE_HARDCOPY === 'true' && process.env.HARDCOPY_PATH) {
|
|
18
|
+
store = new HardcopyMemoryStore(store, process.env.HARDCOPY_PATH);
|
|
19
|
+
console.error(`[hardcopy] Mirroring mutations to ${process.env.HARDCOPY_PATH}`);
|
|
20
|
+
}
|
|
21
|
+
const server = createServer(store);
|
|
22
|
+
// ── Initialise (download model on first run, connect to DB) ──
|
|
23
|
+
await embedder.initialize();
|
|
24
|
+
await store.initialize();
|
|
25
|
+
// ── Start MCP transport ──
|
|
26
|
+
const transport = new StdioServerTransport();
|
|
27
|
+
await server.connect(transport);
|
|
28
|
+
}
|
|
29
|
+
main().catch((err) => {
|
|
30
|
+
console.error('Fatal:', err);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACjE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,yBAAyB,CAAC;IAE3E,6BAA6B;IAC7B,MAAM,QAAQ,GAAG,IAAI,oBAAoB,CAAC,SAAS,CAAC,CAAC;IACrD,IAAI,KAAK,GAAgB,IAAI,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAEhE,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,KAAK,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;QACxE,KAAK,GAAG,IAAI,mBAAmB,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAClE,OAAO,CAAC,KAAK,CAAC,qCAAqC,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAEnC,gEAAgE;IAChE,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC;IAC5B,MAAM,KAAK,CAAC,UAAU,EAAE,CAAC;IAEzB,4BAA4B;IAC5B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Embedder, Memory, MemoryCategory, MemoryStore, MemoryStats, SearchFilters, SearchMode, SearchResult, StoreRequest, UpdateRequest } from './types.js';
|
|
2
|
+
export declare class LanceMemoryStore implements MemoryStore {
|
|
3
|
+
private readonly dbPath;
|
|
4
|
+
private readonly embedder;
|
|
5
|
+
private db;
|
|
6
|
+
private table;
|
|
7
|
+
private ftsIndexCreated;
|
|
8
|
+
private reranker;
|
|
9
|
+
constructor(dbPath: string, embedder: Embedder);
|
|
10
|
+
initialize(): Promise<void>;
|
|
11
|
+
store(request: StoreRequest): Promise<Memory>;
|
|
12
|
+
storeBatch(requests: StoreRequest[]): Promise<Memory[]>;
|
|
13
|
+
search(query: string, mode?: SearchMode, filters?: SearchFilters): Promise<SearchResult[]>;
|
|
14
|
+
findRelated(memoryId: string, limit?: number): Promise<SearchResult[]>;
|
|
15
|
+
listRecent(limit?: number, category?: MemoryCategory): Promise<Memory[]>;
|
|
16
|
+
update(id: string, updates: UpdateRequest): Promise<Memory>;
|
|
17
|
+
delete(id: string): Promise<void>;
|
|
18
|
+
stats(): Promise<MemoryStats>;
|
|
19
|
+
private semanticSearch;
|
|
20
|
+
private keywordSearch;
|
|
21
|
+
private hybridSearch;
|
|
22
|
+
/**
|
|
23
|
+
* Ensures the LanceDB table exists. If the table does not exist, it is
|
|
24
|
+
* created with `seedRow` as initial data (LanceDB requires at least one
|
|
25
|
+
* row to infer the schema). Returns `true` if the table was just created
|
|
26
|
+
* and the seed row was inserted, `false` if the table already existed.
|
|
27
|
+
* Callers MUST check the return value to avoid double-inserting the seed.
|
|
28
|
+
*/
|
|
29
|
+
private ensureTable;
|
|
30
|
+
private tryCreateFtsIndex;
|
|
31
|
+
private fetchById;
|
|
32
|
+
private buildRow;
|
|
33
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import * as lancedb from '@lancedb/lancedb';
|
|
2
|
+
// ── LanceMemoryStore ───────────────────────────────────────────────
|
|
3
|
+
export class LanceMemoryStore {
|
|
4
|
+
dbPath;
|
|
5
|
+
embedder;
|
|
6
|
+
db = null;
|
|
7
|
+
table = null;
|
|
8
|
+
ftsIndexCreated = false;
|
|
9
|
+
reranker = null;
|
|
10
|
+
constructor(dbPath, embedder) {
|
|
11
|
+
this.dbPath = dbPath;
|
|
12
|
+
this.embedder = embedder;
|
|
13
|
+
}
|
|
14
|
+
async initialize() {
|
|
15
|
+
this.db = await lancedb.connect(this.dbPath);
|
|
16
|
+
this.reranker = await lancedb.rerankers.RRFReranker.create(60);
|
|
17
|
+
const names = await this.db.tableNames();
|
|
18
|
+
if (names.includes('memories')) {
|
|
19
|
+
this.table = await this.db.openTable('memories');
|
|
20
|
+
// Recreate FTS index with proper config (stemming, stop words, positions).
|
|
21
|
+
// replace: true makes this idempotent; negligible cost at our scale.
|
|
22
|
+
await this.tryCreateFtsIndex();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// ── Storage ────────────────────────────────────────────────────
|
|
26
|
+
async store(request) {
|
|
27
|
+
const row = await this.buildRow(request);
|
|
28
|
+
const seeded = await this.ensureTable(row);
|
|
29
|
+
if (!seeded) {
|
|
30
|
+
await this.table.add([row]);
|
|
31
|
+
}
|
|
32
|
+
return rowToMemory(row);
|
|
33
|
+
}
|
|
34
|
+
async storeBatch(requests) {
|
|
35
|
+
if (requests.length === 0)
|
|
36
|
+
return [];
|
|
37
|
+
const vectors = await this.embedder.embedBatch(requests.map(r => r.content));
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
const rows = requests.map((req, i) => toRow(req, vectors[i], now));
|
|
40
|
+
const seeded = await this.ensureTable(rows[0]);
|
|
41
|
+
// If the table was just created, rows[0] was already inserted as
|
|
42
|
+
// seed data (LanceDB requires initial data to infer schema).
|
|
43
|
+
const remaining = seeded ? rows.slice(1) : rows;
|
|
44
|
+
if (remaining.length > 0) {
|
|
45
|
+
await this.table.add(remaining);
|
|
46
|
+
}
|
|
47
|
+
return rows.map(rowToMemory);
|
|
48
|
+
}
|
|
49
|
+
// ── Search ─────────────────────────────────────────────────────
|
|
50
|
+
async search(query, mode = 'hybrid', filters = {}) {
|
|
51
|
+
if (!this.table)
|
|
52
|
+
return [];
|
|
53
|
+
const limit = filters.limit ?? 10;
|
|
54
|
+
switch (mode) {
|
|
55
|
+
case 'semantic':
|
|
56
|
+
return this.semanticSearch(query, filters, limit);
|
|
57
|
+
case 'keyword':
|
|
58
|
+
return this.keywordSearch(query, filters, limit);
|
|
59
|
+
case 'hybrid':
|
|
60
|
+
return this.hybridSearch(query, filters, limit);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async findRelated(memoryId, limit = 5) {
|
|
64
|
+
if (!this.table)
|
|
65
|
+
return [];
|
|
66
|
+
const original = await this.fetchById(memoryId);
|
|
67
|
+
if (!original)
|
|
68
|
+
throw new Error(`Memory ${memoryId} not found`);
|
|
69
|
+
const results = await this.table
|
|
70
|
+
.query()
|
|
71
|
+
.nearestTo(original.vector)
|
|
72
|
+
.distanceType('cosine')
|
|
73
|
+
.limit(limit + 1)
|
|
74
|
+
.toArray();
|
|
75
|
+
return results
|
|
76
|
+
.filter((r) => r.id !== memoryId)
|
|
77
|
+
.slice(0, limit)
|
|
78
|
+
.map(resultToSearchResult);
|
|
79
|
+
}
|
|
80
|
+
async listRecent(limit = 10, category) {
|
|
81
|
+
if (!this.table)
|
|
82
|
+
return [];
|
|
83
|
+
let q = this.table.query();
|
|
84
|
+
if (category) {
|
|
85
|
+
q = q.where(`category = '${sanitise(category)}'`);
|
|
86
|
+
}
|
|
87
|
+
const rows = await q.toArray();
|
|
88
|
+
return rows
|
|
89
|
+
.map(rowToMemory)
|
|
90
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
91
|
+
.slice(0, limit);
|
|
92
|
+
}
|
|
93
|
+
// ── Mutation ────────────────────────────────────────────────────
|
|
94
|
+
async update(id, updates) {
|
|
95
|
+
if (!this.table)
|
|
96
|
+
throw new Error('No memories stored yet');
|
|
97
|
+
const existing = await this.fetchById(id);
|
|
98
|
+
if (!existing)
|
|
99
|
+
throw new Error(`Memory ${id} not found`);
|
|
100
|
+
const content = updates.content ?? existing.content;
|
|
101
|
+
const category = updates.category ?? existing.category;
|
|
102
|
+
const tags = updates.tags ?? JSON.parse(existing.tags);
|
|
103
|
+
const now = new Date().toISOString();
|
|
104
|
+
const vector = updates.content
|
|
105
|
+
? await this.embedder.embed(content)
|
|
106
|
+
: existing.vector;
|
|
107
|
+
const updatedRow = {
|
|
108
|
+
id,
|
|
109
|
+
content,
|
|
110
|
+
category,
|
|
111
|
+
tags: JSON.stringify(tags),
|
|
112
|
+
created_at: existing.created_at,
|
|
113
|
+
updated_at: now,
|
|
114
|
+
vector,
|
|
115
|
+
};
|
|
116
|
+
await this.table.delete(`id = '${sanitise(id)}'`);
|
|
117
|
+
await this.table.add([updatedRow]);
|
|
118
|
+
return rowToMemory(updatedRow);
|
|
119
|
+
}
|
|
120
|
+
async delete(id) {
|
|
121
|
+
if (!this.table)
|
|
122
|
+
throw new Error('No memories stored yet');
|
|
123
|
+
await this.table.delete(`id = '${sanitise(id)}'`);
|
|
124
|
+
}
|
|
125
|
+
// ── Stats ──────────────────────────────────────────────────────
|
|
126
|
+
async stats() {
|
|
127
|
+
if (!this.table) {
|
|
128
|
+
return { totalMemories: 0, byCategory: {}, oldestMemory: null, newestMemory: null };
|
|
129
|
+
}
|
|
130
|
+
const rows = (await this.table.query().toArray());
|
|
131
|
+
const memories = rows.map(rowToMemory);
|
|
132
|
+
const byCategory = {};
|
|
133
|
+
for (const m of memories) {
|
|
134
|
+
byCategory[m.category] = (byCategory[m.category] ?? 0) + 1;
|
|
135
|
+
}
|
|
136
|
+
const sorted = [...memories].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
137
|
+
return {
|
|
138
|
+
totalMemories: memories.length,
|
|
139
|
+
byCategory,
|
|
140
|
+
oldestMemory: sorted[0]?.createdAt ?? null,
|
|
141
|
+
newestMemory: sorted.at(-1)?.createdAt ?? null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// ── Private: search strategies ─────────────────────────────────
|
|
145
|
+
async semanticSearch(query, filters, limit) {
|
|
146
|
+
const vector = await this.embedder.embed(query);
|
|
147
|
+
const overFetch = limit * 3;
|
|
148
|
+
let search = this.table.query().nearestTo(vector).distanceType('cosine').limit(overFetch);
|
|
149
|
+
search = applyWhereClause(search, filters);
|
|
150
|
+
const rows = await search.toArray();
|
|
151
|
+
return postFilter(rows, filters).slice(0, limit).map(resultToSearchResult);
|
|
152
|
+
}
|
|
153
|
+
async keywordSearch(query, filters, limit) {
|
|
154
|
+
try {
|
|
155
|
+
const overFetch = limit * 3;
|
|
156
|
+
let search = this.table.search(query, 'fts').limit(overFetch);
|
|
157
|
+
search = applyWhereClause(search, filters);
|
|
158
|
+
const rows = await search.toArray();
|
|
159
|
+
return postFilter(rows, filters).slice(0, limit).map(resultToSearchResult);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// FTS index may not exist yet; keyword search degrades gracefully.
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async hybridSearch(query, filters, limit) {
|
|
167
|
+
if (!this.ftsIndexCreated || !this.reranker) {
|
|
168
|
+
// FTS index not available — degrade to semantic-only.
|
|
169
|
+
return this.semanticSearch(query, filters, limit);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const vector = await this.embedder.embed(query);
|
|
173
|
+
const overFetch = limit * 3;
|
|
174
|
+
let search = this.table
|
|
175
|
+
.query()
|
|
176
|
+
.nearestTo(vector)
|
|
177
|
+
.distanceType('cosine')
|
|
178
|
+
.fullTextSearch(query)
|
|
179
|
+
.rerank(this.reranker)
|
|
180
|
+
.limit(overFetch);
|
|
181
|
+
search = applyWhereClause(search, filters);
|
|
182
|
+
const rows = await search.toArray();
|
|
183
|
+
return postFilter(rows, filters).slice(0, limit).map(resultToSearchResult);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Built-in hybrid can fail if FTS index is stale or missing.
|
|
187
|
+
// Fall back to semantic-only.
|
|
188
|
+
return this.semanticSearch(query, filters, limit);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// ── Private: table management ──────────────────────────────────
|
|
192
|
+
/**
|
|
193
|
+
* Ensures the LanceDB table exists. If the table does not exist, it is
|
|
194
|
+
* created with `seedRow` as initial data (LanceDB requires at least one
|
|
195
|
+
* row to infer the schema). Returns `true` if the table was just created
|
|
196
|
+
* and the seed row was inserted, `false` if the table already existed.
|
|
197
|
+
* Callers MUST check the return value to avoid double-inserting the seed.
|
|
198
|
+
*/
|
|
199
|
+
async ensureTable(seedRow) {
|
|
200
|
+
if (this.table)
|
|
201
|
+
return false;
|
|
202
|
+
this.table = await this.db.createTable('memories', [seedRow]);
|
|
203
|
+
await this.tryCreateFtsIndex();
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
async tryCreateFtsIndex() {
|
|
207
|
+
if (this.ftsIndexCreated || !this.table)
|
|
208
|
+
return;
|
|
209
|
+
try {
|
|
210
|
+
await this.table.createIndex('content', {
|
|
211
|
+
config: lancedb.Index.fts({
|
|
212
|
+
withPosition: true,
|
|
213
|
+
stem: true,
|
|
214
|
+
language: 'English',
|
|
215
|
+
removeStopWords: true,
|
|
216
|
+
asciiFolding: true,
|
|
217
|
+
}),
|
|
218
|
+
replace: true,
|
|
219
|
+
});
|
|
220
|
+
this.ftsIndexCreated = true;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Index creation may fail on very small tables; keyword search degrades gracefully.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async fetchById(id) {
|
|
227
|
+
if (!this.table)
|
|
228
|
+
return null;
|
|
229
|
+
const rows = await this.table.query().where(`id = '${sanitise(id)}'`).limit(1).toArray();
|
|
230
|
+
return rows[0] ?? null;
|
|
231
|
+
}
|
|
232
|
+
async buildRow(request) {
|
|
233
|
+
const vector = await this.embedder.embed(request.content);
|
|
234
|
+
const now = new Date().toISOString();
|
|
235
|
+
return toRow(request, vector, now);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// ── Pure functions ─────────────────────────────────────────────────
|
|
239
|
+
function toRow(request, vector, timestamp) {
|
|
240
|
+
return {
|
|
241
|
+
id: crypto.randomUUID(),
|
|
242
|
+
content: request.content,
|
|
243
|
+
category: request.category,
|
|
244
|
+
tags: JSON.stringify(request.tags),
|
|
245
|
+
created_at: timestamp,
|
|
246
|
+
updated_at: timestamp,
|
|
247
|
+
vector,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function rowToMemory(row) {
|
|
251
|
+
return {
|
|
252
|
+
id: row.id,
|
|
253
|
+
content: row.content,
|
|
254
|
+
category: row.category,
|
|
255
|
+
tags: JSON.parse(row.tags),
|
|
256
|
+
createdAt: row.created_at,
|
|
257
|
+
updatedAt: row.updated_at,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function resultToSearchResult(row) {
|
|
261
|
+
// Three possible score fields depending on search mode:
|
|
262
|
+
// _relevance_score — from the RRF reranker (hybrid search), higher = better
|
|
263
|
+
// _distance — from vector search (cosine: 0–2 range), lower = better
|
|
264
|
+
// _score — from FTS/BM25 search, higher = better
|
|
265
|
+
const relevanceScore = row._relevance_score;
|
|
266
|
+
const distance = row._distance;
|
|
267
|
+
const ftsScore = row._score;
|
|
268
|
+
let score;
|
|
269
|
+
if (relevanceScore != null) {
|
|
270
|
+
score = relevanceScore;
|
|
271
|
+
}
|
|
272
|
+
else if (distance != null) {
|
|
273
|
+
score = 1 / (1 + distance);
|
|
274
|
+
}
|
|
275
|
+
else if (ftsScore != null) {
|
|
276
|
+
score = ftsScore;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
score = 0;
|
|
280
|
+
}
|
|
281
|
+
return { memory: rowToMemory(row), score };
|
|
282
|
+
}
|
|
283
|
+
function applyWhereClause(search, filters) {
|
|
284
|
+
const clauses = [];
|
|
285
|
+
if (filters.category) {
|
|
286
|
+
clauses.push(`category = '${sanitise(filters.category)}'`);
|
|
287
|
+
}
|
|
288
|
+
if (filters.after) {
|
|
289
|
+
clauses.push(`created_at >= '${sanitise(filters.after)}'`);
|
|
290
|
+
}
|
|
291
|
+
if (filters.before) {
|
|
292
|
+
clauses.push(`created_at <= '${sanitise(filters.before)}'`);
|
|
293
|
+
}
|
|
294
|
+
if (clauses.length > 0) {
|
|
295
|
+
return search.where(clauses.join(' AND '));
|
|
296
|
+
}
|
|
297
|
+
return search;
|
|
298
|
+
}
|
|
299
|
+
function postFilter(rows, filters) {
|
|
300
|
+
if (!filters.tags || filters.tags.length === 0)
|
|
301
|
+
return rows;
|
|
302
|
+
return rows.filter(row => {
|
|
303
|
+
const tags = JSON.parse(row.tags);
|
|
304
|
+
return filters.tags.some(t => tags.includes(t));
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function sanitise(value) {
|
|
308
|
+
return value.replace(/'/g, "''");
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=memory-store.js.map
|