memshell 0.1.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 +21 -0
- package/README.md +157 -0
- package/bin/mem.js +102 -0
- package/package.json +37 -0
- package/server.js +63 -0
- package/src/index.js +180 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 justedv
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<h1>mem.sh</h1>
|
|
4
|
+
|
|
5
|
+
<p><strong>Persistent memory for AI agents.</strong><br>One line to save. One line to recall.</p>
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/mem.sh)
|
|
8
|
+
[](https://github.com/justedv/mem.sh/blob/main/LICENSE)
|
|
9
|
+
[](https://www.npmjs.com/package/mem.sh)
|
|
10
|
+
[](https://github.com/justedv/mem.sh)
|
|
11
|
+
|
|
12
|
+
<br>
|
|
13
|
+
|
|
14
|
+
[Quick Start](#quick-start) · [SDK](#sdk) · [API Server](#api-server) · [Architecture](#how-it-works) · [Contributing](CONTRIBUTING.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why mem.sh?
|
|
21
|
+
|
|
22
|
+
Agents forget everything between sessions. **mem.sh** gives them a brain.
|
|
23
|
+
|
|
24
|
+
| | mem.sh | LangChain Memory | Roll your own |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| **Setup** | `npx mem.sh set "..."` | 47 dependencies + config | Hours of boilerplate |
|
|
27
|
+
| **External APIs** | None | OpenAI key required | Depends |
|
|
28
|
+
| **Semantic search** | Built-in TF-IDF | Embedding models | You build it |
|
|
29
|
+
| **Storage** | SQLite (local) | Varies | You choose |
|
|
30
|
+
| **Lines of code** | ~1 | ~50+ | ~200+ |
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Fast** — TF-IDF vectorization with cosine similarity, instant results
|
|
35
|
+
- **Local-first** — SQLite storage at `~/.mem/mem.db`, no data leaves your machine
|
|
36
|
+
- **Semantic** — Recall by meaning, not exact match
|
|
37
|
+
- **Zero config** — `npx` and go. No API keys, no setup, no dependencies
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### CLI
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Store a memory
|
|
45
|
+
npx mem.sh set "user prefers dark mode"
|
|
46
|
+
|
|
47
|
+
# Recall semantically
|
|
48
|
+
npx mem.sh recall "what theme does the user like?"
|
|
49
|
+
# → user prefers dark mode (score: 0.87)
|
|
50
|
+
|
|
51
|
+
# List all memories
|
|
52
|
+
npx mem.sh list
|
|
53
|
+
|
|
54
|
+
# Forget a specific memory
|
|
55
|
+
npx mem.sh forget <id>
|
|
56
|
+
|
|
57
|
+
# Wipe everything
|
|
58
|
+
npx mem.sh clear
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### SDK
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const mem = require('mem.sh');
|
|
65
|
+
|
|
66
|
+
// Store
|
|
67
|
+
await mem.set('user prefers dark mode');
|
|
68
|
+
await mem.set('favorite language is rust', { agent: 'coder-bot' });
|
|
69
|
+
|
|
70
|
+
// Recall (semantic search)
|
|
71
|
+
const results = await mem.recall('what does the user like?');
|
|
72
|
+
// [{ id, text, score, created_at }]
|
|
73
|
+
|
|
74
|
+
// List all
|
|
75
|
+
const all = await mem.list();
|
|
76
|
+
|
|
77
|
+
// Delete
|
|
78
|
+
await mem.forget(id);
|
|
79
|
+
|
|
80
|
+
// Clear everything
|
|
81
|
+
await mem.clear();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## How It Works
|
|
85
|
+
|
|
86
|
+
mem.sh uses **TF-IDF vectorization** with **cosine similarity** for semantic search. No OpenAI key needed. No external APIs. Everything runs locally.
|
|
87
|
+
|
|
88
|
+
Memories are stored in `~/.mem/mem.db` (SQLite). Each memory is tokenized and vectorized on write. Queries are vectorized at recall time and ranked by cosine similarity against stored vectors.
|
|
89
|
+
|
|
90
|
+
→ [Full architecture docs](docs/ARCHITECTURE.md)
|
|
91
|
+
|
|
92
|
+
## API Server
|
|
93
|
+
|
|
94
|
+
Run a shared memory server for multiple agents:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx mem.sh serve --port 3456 --key my-secret-key
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Endpoints
|
|
101
|
+
|
|
102
|
+
| Method | Path | Description |
|
|
103
|
+
|--------|------|-------------|
|
|
104
|
+
| `POST` | `/mem` | Store a memory |
|
|
105
|
+
| `GET` | `/mem/recall?q=` | Semantic recall |
|
|
106
|
+
| `GET` | `/mem/list` | List all memories |
|
|
107
|
+
| `DELETE` | `/mem/:id` | Delete a memory |
|
|
108
|
+
| `DELETE` | `/mem` | Clear all memories |
|
|
109
|
+
|
|
110
|
+
### Headers
|
|
111
|
+
|
|
112
|
+
- `X-Mem-Key` — API key (required if `--key` is set)
|
|
113
|
+
- `X-Mem-Agent` — Agent namespace (optional, isolates memories per agent)
|
|
114
|
+
|
|
115
|
+
### Example
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Store
|
|
119
|
+
curl -X POST http://localhost:3456/mem \
|
|
120
|
+
-H "Content-Type: application/json" \
|
|
121
|
+
-H "X-Mem-Key: my-secret-key" \
|
|
122
|
+
-d '{"text": "user prefers dark mode"}'
|
|
123
|
+
|
|
124
|
+
# Recall
|
|
125
|
+
curl "http://localhost:3456/mem/recall?q=theme+preference" \
|
|
126
|
+
-H "X-Mem-Key: my-secret-key"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### SDK with API Mode
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const mem = require('mem.sh');
|
|
133
|
+
|
|
134
|
+
mem.configure({
|
|
135
|
+
api: 'http://localhost:3456',
|
|
136
|
+
key: 'my-secret-key',
|
|
137
|
+
agent: 'my-bot'
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await mem.set('user prefers dark mode');
|
|
141
|
+
const results = await mem.recall('theme preference');
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Contributing
|
|
145
|
+
|
|
146
|
+
We welcome contributions. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
[ISC](LICENSE)
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
<div align="center">
|
|
155
|
+
<sub>Built for agents that need to remember.</sub><br>
|
|
156
|
+
<a href="https://www.npmjs.com/package/mem.sh">npm</a> · <a href="https://github.com/justedv/mem.sh">GitHub</a> · <a href="https://github.com/justedv/mem.sh/issues">Issues</a>
|
|
157
|
+
</div>
|
package/bin/mem.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const mem = require('../src/index');
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const cmd = args[0];
|
|
8
|
+
const rest = args.slice(1).join(' ');
|
|
9
|
+
|
|
10
|
+
const HELP = `
|
|
11
|
+
mem.sh — persistent memory for AI agents
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
mem set <text> Store a memory
|
|
15
|
+
mem recall <query> Semantic recall
|
|
16
|
+
mem list List all memories
|
|
17
|
+
mem forget <id> Delete a memory by ID
|
|
18
|
+
mem clear Wipe all memories
|
|
19
|
+
mem serve [--port N] Start API server
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--agent <name> Agent namespace
|
|
23
|
+
--api <url> Use remote API instead of local
|
|
24
|
+
--key <key> API key for remote server
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
mem set "user prefers dark mode"
|
|
28
|
+
mem recall "what theme?"
|
|
29
|
+
mem list
|
|
30
|
+
mem forget 3
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
// Parse flags
|
|
34
|
+
function flag(name) {
|
|
35
|
+
const i = args.indexOf('--' + name);
|
|
36
|
+
if (i === -1) return null;
|
|
37
|
+
return args[i + 1] || true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const agent = flag('agent') || 'default';
|
|
42
|
+
const api = flag('api');
|
|
43
|
+
const key = flag('key');
|
|
44
|
+
|
|
45
|
+
if (api) mem.configure({ api, key, agent });
|
|
46
|
+
else mem.configure({ agent });
|
|
47
|
+
|
|
48
|
+
const opts = { agent };
|
|
49
|
+
|
|
50
|
+
switch (cmd) {
|
|
51
|
+
case 'set': case 's': case 'save': case 'remember': {
|
|
52
|
+
const text = args.slice(1).filter(a => !a.startsWith('--')).join(' ').replace(/^["']|["']$/g, '');
|
|
53
|
+
if (!text) return console.log('Usage: mem set <text>');
|
|
54
|
+
const r = await mem.set(text, opts);
|
|
55
|
+
console.log(`✓ Stored (id: ${r.id})`);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case 'recall': case 'r': case 'search': case 'q': {
|
|
59
|
+
const query = args.slice(1).filter(a => !a.startsWith('--')).join(' ').replace(/^["']|["']$/g, '');
|
|
60
|
+
if (!query) return console.log('Usage: mem recall <query>');
|
|
61
|
+
const results = await mem.recall(query, opts);
|
|
62
|
+
if (!results.length) return console.log('No memories found.');
|
|
63
|
+
for (const r of results) {
|
|
64
|
+
console.log(` [${r.id}] ${r.text} (score: ${r.score})`);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'list': case 'ls': case 'l': {
|
|
69
|
+
const all = await mem.list(opts);
|
|
70
|
+
if (!all.length) return console.log('No memories stored.');
|
|
71
|
+
for (const r of all) {
|
|
72
|
+
console.log(` [${r.id}] ${r.text} (${r.created_at})`);
|
|
73
|
+
}
|
|
74
|
+
console.log(`\n ${all.length} memor${all.length === 1 ? 'y' : 'ies'}`);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case 'forget': case 'delete': case 'rm': {
|
|
78
|
+
const id = args[1];
|
|
79
|
+
if (!id) return console.log('Usage: mem forget <id>');
|
|
80
|
+
await mem.forget(id);
|
|
81
|
+
console.log(`✓ Forgotten (id: ${id})`);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'clear': case 'wipe': case 'reset': {
|
|
85
|
+
await mem.clear(opts);
|
|
86
|
+
console.log('✓ All memories cleared');
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case 'serve': case 'server': {
|
|
90
|
+
const port = flag('port') || 3456;
|
|
91
|
+
const authKey = flag('key') || process.env.MEM_KEY || '';
|
|
92
|
+
process.env.MEM_PORT = port;
|
|
93
|
+
if (authKey) process.env.MEM_KEY = authKey;
|
|
94
|
+
require('../server');
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
console.log(HELP);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main().catch(e => { console.error('Error:', e.message); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memshell",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Persistent memory for AI agents. Like localStorage but for LLMs.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mem": "./bin/mem.js",
|
|
8
|
+
"mem.sh": "./bin/mem.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node server.js",
|
|
12
|
+
"test": "node test.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai",
|
|
16
|
+
"agents",
|
|
17
|
+
"memory",
|
|
18
|
+
"llm",
|
|
19
|
+
"semantic-search",
|
|
20
|
+
"embeddings",
|
|
21
|
+
"chatgpt",
|
|
22
|
+
"langchain",
|
|
23
|
+
"vector",
|
|
24
|
+
"recall"
|
|
25
|
+
],
|
|
26
|
+
"author": "justedv",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"better-sqlite3": "^11.0.0",
|
|
30
|
+
"express": "^4.18.2"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/justedv/mem.sh"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://justedv.github.io/mem.sh"
|
|
37
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const { LocalStore } = require('./src/index');
|
|
5
|
+
|
|
6
|
+
const app = express();
|
|
7
|
+
app.use(express.json());
|
|
8
|
+
|
|
9
|
+
const store = new LocalStore();
|
|
10
|
+
const PORT = process.env.MEM_PORT || 3456;
|
|
11
|
+
const AUTH_KEY = process.env.MEM_KEY || '';
|
|
12
|
+
|
|
13
|
+
// Auth middleware
|
|
14
|
+
app.use('/mem', (req, res, next) => {
|
|
15
|
+
if (AUTH_KEY && req.headers['x-mem-key'] !== AUTH_KEY) {
|
|
16
|
+
return res.status(401).json({ error: 'Invalid API key' });
|
|
17
|
+
}
|
|
18
|
+
req.agent = req.headers['x-mem-agent'] || 'default';
|
|
19
|
+
next();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Store a memory
|
|
23
|
+
app.post('/mem', (req, res) => {
|
|
24
|
+
const { text, metadata } = req.body;
|
|
25
|
+
if (!text) return res.status(400).json({ error: 'text is required' });
|
|
26
|
+
const result = store.set(text, { agent: req.agent, metadata });
|
|
27
|
+
res.json(result);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Semantic recall
|
|
31
|
+
app.get('/mem/recall', (req, res) => {
|
|
32
|
+
const q = req.query.q;
|
|
33
|
+
if (!q) return res.status(400).json({ error: 'q parameter is required' });
|
|
34
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
35
|
+
const results = store.recall(q, { agent: req.agent, limit });
|
|
36
|
+
res.json(results);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// List all
|
|
40
|
+
app.get('/mem/list', (req, res) => {
|
|
41
|
+
const results = store.list({ agent: req.agent });
|
|
42
|
+
res.json(results);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Delete by id
|
|
46
|
+
app.delete('/mem/:id', (req, res) => {
|
|
47
|
+
store.forget(req.params.id);
|
|
48
|
+
res.json({ ok: true, id: req.params.id });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Clear all for agent
|
|
52
|
+
app.delete('/mem', (req, res) => {
|
|
53
|
+
store.clear({ agent: req.agent });
|
|
54
|
+
res.json({ ok: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.listen(PORT, () => {
|
|
58
|
+
console.log(`\n 🧠 mem.sh server running on http://localhost:${PORT}`);
|
|
59
|
+
console.log(` Auth: ${AUTH_KEY ? 'enabled' : 'disabled'}`);
|
|
60
|
+
console.log();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
module.exports = app;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
|
|
9
|
+
// ── TF-IDF Engine ──────────────────────────────────────────────
|
|
10
|
+
class TfIdf {
|
|
11
|
+
tokenize(text) {
|
|
12
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
tf(tokens) {
|
|
16
|
+
const freq = {};
|
|
17
|
+
for (const t of tokens) freq[t] = (freq[t] || 0) + 1;
|
|
18
|
+
const len = tokens.length || 1;
|
|
19
|
+
for (const t in freq) freq[t] /= len;
|
|
20
|
+
return freq;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
idf(docs) {
|
|
24
|
+
const df = {};
|
|
25
|
+
const n = docs.length || 1;
|
|
26
|
+
for (const doc of docs) {
|
|
27
|
+
const seen = new Set(doc);
|
|
28
|
+
for (const t of seen) df[t] = (df[t] || 0) + 1;
|
|
29
|
+
}
|
|
30
|
+
const idf = {};
|
|
31
|
+
for (const t in df) idf[t] = Math.log((n + 1) / (df[t] + 1)) + 1;
|
|
32
|
+
return idf;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
vectorize(tf, idf) {
|
|
36
|
+
const vec = {};
|
|
37
|
+
for (const t in tf) vec[t] = tf[t] * (idf[t] || Math.log(2));
|
|
38
|
+
return vec;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
cosine(a, b) {
|
|
42
|
+
let dot = 0, magA = 0, magB = 0;
|
|
43
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
44
|
+
for (const k of keys) {
|
|
45
|
+
const va = a[k] || 0, vb = b[k] || 0;
|
|
46
|
+
dot += va * vb;
|
|
47
|
+
magA += va * va;
|
|
48
|
+
magB += vb * vb;
|
|
49
|
+
}
|
|
50
|
+
if (!magA || !magB) return 0;
|
|
51
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
rank(query, documents) {
|
|
55
|
+
const qTokens = this.tokenize(query);
|
|
56
|
+
const docTokens = documents.map(d => this.tokenize(d.text));
|
|
57
|
+
const allTokens = [qTokens, ...docTokens];
|
|
58
|
+
const idfScores = this.idf(allTokens);
|
|
59
|
+
const qVec = this.vectorize(this.tf(qTokens), idfScores);
|
|
60
|
+
return documents.map((doc, i) => {
|
|
61
|
+
const dVec = this.vectorize(this.tf(docTokens[i]), idfScores);
|
|
62
|
+
return { ...doc, score: Math.round(this.cosine(qVec, dVec) * 1000) / 1000 };
|
|
63
|
+
}).filter(d => d.score > 0.01).sort((a, b) => b.score - a.score);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Local Store ────────────────────────────────────────────────
|
|
68
|
+
class LocalStore {
|
|
69
|
+
constructor(dir) {
|
|
70
|
+
this.dir = dir || path.join(os.homedir(), '.mem');
|
|
71
|
+
this.dbPath = path.join(this.dir, 'mem.db');
|
|
72
|
+
this.tfidf = new TfIdf();
|
|
73
|
+
this._db = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get db() {
|
|
77
|
+
if (!this._db) {
|
|
78
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
79
|
+
const Database = require('better-sqlite3');
|
|
80
|
+
this._db = new Database(this.dbPath);
|
|
81
|
+
this._db.pragma('journal_mode = WAL');
|
|
82
|
+
this._db.exec(`
|
|
83
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
84
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
85
|
+
text TEXT NOT NULL,
|
|
86
|
+
agent TEXT DEFAULT 'default',
|
|
87
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
88
|
+
metadata TEXT DEFAULT '{}'
|
|
89
|
+
)
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
return this._db;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
set(text, opts = {}) {
|
|
96
|
+
const agent = opts.agent || 'default';
|
|
97
|
+
const meta = JSON.stringify(opts.metadata || {});
|
|
98
|
+
const info = this.db.prepare('INSERT INTO memories (text, agent, metadata) VALUES (?, ?, ?)').run(text, agent, meta);
|
|
99
|
+
return { id: info.lastInsertRowid, text, agent };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
recall(query, opts = {}) {
|
|
103
|
+
const agent = opts.agent || 'default';
|
|
104
|
+
const limit = opts.limit || 10;
|
|
105
|
+
const rows = this.db.prepare('SELECT * FROM memories WHERE agent = ?').all(agent);
|
|
106
|
+
return this.tfidf.rank(query, rows).slice(0, limit);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
list(opts = {}) {
|
|
110
|
+
const agent = opts.agent || 'default';
|
|
111
|
+
return this.db.prepare('SELECT * FROM memories WHERE agent = ? ORDER BY created_at DESC').all(agent);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
forget(id) {
|
|
115
|
+
return this.db.prepare('DELETE FROM memories WHERE id = ?').run(id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
clear(opts = {}) {
|
|
119
|
+
const agent = opts.agent || 'default';
|
|
120
|
+
return this.db.prepare('DELETE FROM memories WHERE agent = ?').run(agent);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── API Client ─────────────────────────────────────────────────
|
|
125
|
+
class ApiClient {
|
|
126
|
+
constructor(opts) {
|
|
127
|
+
this.base = opts.api.replace(/\/$/, '');
|
|
128
|
+
this.key = opts.key || '';
|
|
129
|
+
this.agent = opts.agent || 'default';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_req(method, path, body) {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const url = new URL(this.base + path);
|
|
135
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
136
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
137
|
+
if (this.key) headers['X-Mem-Key'] = this.key;
|
|
138
|
+
headers['X-Mem-Agent'] = this.agent;
|
|
139
|
+
const req = mod.request(url, { method, headers }, res => {
|
|
140
|
+
let d = '';
|
|
141
|
+
res.on('data', c => d += c);
|
|
142
|
+
res.on('end', () => {
|
|
143
|
+
try { resolve(JSON.parse(d)); } catch { resolve(d); }
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
req.on('error', reject);
|
|
147
|
+
if (body) req.write(JSON.stringify(body));
|
|
148
|
+
req.end();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
set(text, opts = {}) { return this._req('POST', '/mem', { text, metadata: opts.metadata }); }
|
|
153
|
+
recall(query, opts = {}) { return this._req('GET', `/mem/recall?q=${encodeURIComponent(query)}&limit=${opts.limit || 10}`); }
|
|
154
|
+
list() { return this._req('GET', '/mem/list'); }
|
|
155
|
+
forget(id) { return this._req('DELETE', `/mem/${id}`); }
|
|
156
|
+
clear() { return this._req('DELETE', '/mem'); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Exports ────────────────────────────────────────────────────
|
|
160
|
+
let _config = {};
|
|
161
|
+
let _store = null;
|
|
162
|
+
|
|
163
|
+
function getStore() {
|
|
164
|
+
if (!_store) {
|
|
165
|
+
_store = _config.api ? new ApiClient(_config) : new LocalStore(_config.dir);
|
|
166
|
+
}
|
|
167
|
+
return _store;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
configure(opts) { _config = opts; _store = null; },
|
|
172
|
+
set(text, opts) { return Promise.resolve(getStore().set(text, opts)); },
|
|
173
|
+
recall(query, opts) { return Promise.resolve(getStore().recall(query, opts)); },
|
|
174
|
+
list(opts) { return Promise.resolve(getStore().list(opts)); },
|
|
175
|
+
forget(id) { return Promise.resolve(getStore().forget(id)); },
|
|
176
|
+
clear(opts) { return Promise.resolve(getStore().clear(opts)); },
|
|
177
|
+
TfIdf,
|
|
178
|
+
LocalStore,
|
|
179
|
+
ApiClient
|
|
180
|
+
};
|