@j0hanz/memdb 1.0.11 → 1.1.1
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 +82 -120
- package/dist/config.d.ts +4 -0
- package/dist/config.js +13 -0
- package/dist/core/database-schema.d.ts +0 -1
- package/dist/core/database-schema.js +0 -1
- package/dist/core/database.d.ts +0 -1
- package/dist/core/database.js +0 -1
- package/dist/core/db.d.ts +16 -0
- package/dist/core/db.js +232 -0
- package/dist/core/memory-create.d.ts +0 -1
- package/dist/core/memory-create.js +13 -7
- package/dist/core/memory-db.d.ts +0 -1
- package/dist/core/memory-db.js +0 -1
- package/dist/core/memory-read.d.ts +2 -2
- package/dist/core/memory-read.js +32 -4
- package/dist/core/memory-relations.d.ts +0 -1
- package/dist/core/memory-relations.js +0 -1
- package/dist/core/memory-search.d.ts +8 -7
- package/dist/core/memory-search.js +15 -9
- package/dist/core/memory-stats.d.ts +0 -1
- package/dist/core/memory-stats.js +0 -1
- package/dist/core/memory-updates.d.ts +0 -1
- package/dist/core/memory-updates.js +1 -2
- package/dist/core/memory-write.d.ts +13 -0
- package/dist/core/memory-write.js +113 -0
- package/dist/core/relation-queries.d.ts +0 -1
- package/dist/core/relation-queries.js +0 -1
- package/dist/core/relations.d.ts +10 -0
- package/dist/core/relations.js +177 -0
- package/dist/core/row-mappers.d.ts +0 -1
- package/dist/core/row-mappers.js +0 -1
- package/dist/core/search-errors.d.ts +0 -1
- package/dist/core/search-errors.js +0 -1
- package/dist/core/search.d.ts +5 -12
- package/dist/core/search.js +77 -56
- package/dist/core/sqlite.d.ts +0 -1
- package/dist/core/sqlite.js +0 -3
- package/dist/core/tags.d.ts +0 -1
- package/dist/core/tags.js +1 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +58 -37
- package/dist/lib/errors.d.ts +0 -1
- package/dist/lib/errors.js +0 -1
- package/dist/logger.d.ts +5 -0
- package/dist/logger.js +17 -0
- package/dist/protocol-version-guard.d.ts +17 -0
- package/dist/protocol-version-guard.js +100 -0
- package/dist/schemas/inputs.d.ts +0 -1
- package/dist/schemas/inputs.js +0 -1
- package/dist/schemas/outputs.d.ts +0 -1
- package/dist/schemas/outputs.js +0 -1
- package/dist/schemas.d.ts +28 -0
- package/dist/schemas.js +65 -0
- package/dist/tools/definitions/memory-core.d.ts +0 -1
- package/dist/tools/definitions/memory-core.js +0 -1
- package/dist/tools/definitions/memory-relations.d.ts +0 -1
- package/dist/tools/definitions/memory-relations.js +0 -1
- package/dist/tools/definitions/memory-search.d.ts +0 -1
- package/dist/tools/definitions/memory-search.js +1 -11
- package/dist/tools/definitions/memory-stats.d.ts +0 -1
- package/dist/tools/definitions/memory-stats.js +0 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -1
- package/dist/tools/tool-handlers.d.ts +0 -1
- package/dist/tools/tool-handlers.js +0 -1
- package/dist/tools/tool-types.d.ts +0 -1
- package/dist/tools/tool-types.js +0 -1
- package/dist/tools.d.ts +18 -0
- package/dist/tools.js +167 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/dist/utils/config.d.ts +0 -1
- package/dist/utils/config.js +0 -1
- package/dist/utils/logger.d.ts +0 -1
- package/dist/utils/logger.js +0 -1
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +118 -0
- package/dist/worker/db-worker-client.d.ts +9 -0
- package/dist/worker/db-worker-client.js +93 -0
- package/dist/worker/db-worker.d.ts +1 -0
- package/dist/worker/db-worker.js +174 -0
- package/dist/worker/protocol.d.ts +9 -0
- package/dist/worker/protocol.js +14 -0
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# memdb
|
|
2
2
|
|
|
3
|
-
A SQLite-backed MCP memory server
|
|
3
|
+
A SQLite-backed MCP memory server with local workspace storage.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@j0hanz/memdb)
|
|
6
6
|
|
|
@@ -12,13 +12,12 @@ A SQLite-backed MCP memory server (on-disk by default, in-memory optional).
|
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
|
-
| Feature
|
|
16
|
-
|
|
|
17
|
-
| Memory Storage
|
|
18
|
-
| Full-Text Search
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
| Local Privacy | All data stored locally in SQLite (`.memdb/memory.db` by default) |
|
|
15
|
+
| Feature | Description |
|
|
16
|
+
| :--------------- | :---------------------------------------------------------------- |
|
|
17
|
+
| Memory Storage | Store text memories with tags |
|
|
18
|
+
| Full-Text Search | FTS5-backed tokenized search with relevance ranking |
|
|
19
|
+
| Stats | Memory and tag counts + activity range |
|
|
20
|
+
| Local Privacy | All data stored locally in SQLite (`.memdb/memory.db` by default) |
|
|
22
21
|
|
|
23
22
|
## Quick Start
|
|
24
23
|
|
|
@@ -58,25 +57,21 @@ npm install
|
|
|
58
57
|
npm run build
|
|
59
58
|
```
|
|
60
59
|
|
|
61
|
-
##
|
|
60
|
+
## Storage
|
|
62
61
|
|
|
63
|
-
The server uses a local SQLite database at `<cwd>/.memdb/memory.db
|
|
64
|
-
|
|
62
|
+
The server uses a local SQLite database at `<cwd>/.memdb/memory.db`. The
|
|
63
|
+
directory is created automatically when needed. All data remains local to your
|
|
64
|
+
workspace.
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
> **Tip:** Add `.memdb/` to your `.gitignore` to keep your memory database out of
|
|
67
|
+
> version control:
|
|
68
|
+
>
|
|
69
|
+
> ```bash
|
|
70
|
+
> echo ".memdb/" >> .gitignore
|
|
71
|
+
> ```
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- `MEMDB_SHUTDOWN_TIMEOUT`: Shutdown timeout in ms (1000-60000, default: `5000`).
|
|
71
|
-
|
|
72
|
-
### CLI Flags
|
|
73
|
-
|
|
74
|
-
- `--db <path>`: Override the database path.
|
|
75
|
-
- `--memory`: Use in-memory database (`:memory:`).
|
|
76
|
-
- `--log-level <level>`: `info`, `warn`, or `error`.
|
|
77
|
-
- `--shutdown-timeout <ms>`: Shutdown timeout in ms (1000-60000).
|
|
78
|
-
|
|
79
|
-
Precedence: CLI flags > environment variables > defaults.
|
|
73
|
+
To completely reset or remove all stored memories, simply delete the `.memdb/`
|
|
74
|
+
folder. The server will create a fresh database on the next run.
|
|
80
75
|
|
|
81
76
|
## Tool Response Format
|
|
82
77
|
|
|
@@ -117,35 +112,36 @@ Example `content[0].text`:
|
|
|
117
112
|
|
|
118
113
|
### `store_memory`
|
|
119
114
|
|
|
120
|
-
Store a new memory with
|
|
115
|
+
Store a new memory with tags.
|
|
121
116
|
|
|
122
|
-
| Parameter
|
|
123
|
-
|
|
|
124
|
-
| `content`
|
|
125
|
-
| `tags`
|
|
126
|
-
| `importance` | number | No | `0` | Importance score (0-10) |
|
|
127
|
-
| `memoryType` | string | No | `general` | Type of memory (1-50 chars) |
|
|
117
|
+
| Parameter | Type | Required | Default | Description |
|
|
118
|
+
| :-------- | :------- | :------- | :------ | :-------------------------------------------------- |
|
|
119
|
+
| `content` | string | Yes | - | The content of the memory (1-100000 chars) |
|
|
120
|
+
| `tags` | string[] | Yes | - | Tags (1-100 tags, no whitespace, max 50 chars each) |
|
|
128
121
|
|
|
129
122
|
**Returns:** `{ id, hash, isNew }`
|
|
130
123
|
|
|
131
124
|
Notes:
|
|
132
125
|
|
|
133
126
|
- Content is deduplicated by MD5 hash. Storing the same content again returns the same hash with `isNew: false`.
|
|
127
|
+
- Tags must not contain whitespace. Use hyphens for compound words (e.g., `api-design`, `error-handling`).
|
|
134
128
|
|
|
135
129
|
### `search_memories`
|
|
136
130
|
|
|
137
|
-
|
|
131
|
+
Search memories by content and tags.
|
|
138
132
|
|
|
139
|
-
| Parameter
|
|
140
|
-
|
|
|
141
|
-
| `query`
|
|
142
|
-
| `limit` | number | No | `10` | Maximum number of results (1-100) |
|
|
143
|
-
| `offset` | number | No | `0` | Pagination offset (0-1000) |
|
|
144
|
-
| `tags` | string[] | No | - | Filter by tags (max 50) |
|
|
145
|
-
| `minRelevance` | number | No | - | Minimum relevance score (0-1) |
|
|
133
|
+
| Parameter | Type | Required | Default | Description |
|
|
134
|
+
| :-------- | :----- | :------- | :------ | :---------------------------------------- |
|
|
135
|
+
| `query` | string | Yes | - | Search query (1-1000 chars, max 50 terms) |
|
|
146
136
|
|
|
147
137
|
**Returns:** Array of search results (`Memory` + `relevance`).
|
|
148
138
|
|
|
139
|
+
Notes:
|
|
140
|
+
|
|
141
|
+
- Searches both memory content (full-text) and tags.
|
|
142
|
+
- Returns up to 100 results, ranked by relevance.
|
|
143
|
+
- Content matches rank higher than tag matches.
|
|
144
|
+
|
|
149
145
|
### `get_memory`
|
|
150
146
|
|
|
151
147
|
Retrieve a specific memory by its hash.
|
|
@@ -166,58 +162,31 @@ Delete a memory by its hash.
|
|
|
166
162
|
|
|
167
163
|
**Returns:** `{ deleted: true }`.
|
|
168
164
|
|
|
169
|
-
### `link_memories`
|
|
170
|
-
|
|
171
|
-
Create a relationship between two memories.
|
|
172
|
-
|
|
173
|
-
| Parameter | Type | Required | Default | Description |
|
|
174
|
-
| :------------- | :----- | :------- | :------ | :----------------------------------- |
|
|
175
|
-
| `fromHash` | string | Yes | - | Hash of the source memory (32 chars) |
|
|
176
|
-
| `toHash` | string | Yes | - | Hash of the target memory (32 chars) |
|
|
177
|
-
| `relationType` | string | Yes | - | Type of relationship (1-50 chars) |
|
|
178
|
-
|
|
179
|
-
**Returns:** `{ linked: true }`.
|
|
180
|
-
|
|
181
|
-
Notes:
|
|
182
|
-
|
|
183
|
-
- Linking the same relation again is a no-op (idempotent).
|
|
184
|
-
- Returns an error if either memory hash does not exist.
|
|
185
|
-
|
|
186
|
-
### `get_related`
|
|
187
|
-
|
|
188
|
-
Get memories related to a given memory.
|
|
189
|
-
|
|
190
|
-
| Parameter | Type | Required | Default | Description |
|
|
191
|
-
| :------------- | :----- | :------- | :--------- | :----------------------------- |
|
|
192
|
-
| `hash` | string | Yes | - | Hash of the memory (32 chars) |
|
|
193
|
-
| `relationType` | string | No | - | Filter by relationship type |
|
|
194
|
-
| `depth` | number | No | `1` | Traversal depth (1-3) |
|
|
195
|
-
| `direction` | string | No | `outgoing` | `outgoing`, `incoming`, `both` |
|
|
196
|
-
|
|
197
|
-
**Returns:** Array of related memories (`Memory` + `relation_type`, `depth`).
|
|
198
|
-
|
|
199
165
|
### `memory_stats`
|
|
200
166
|
|
|
201
|
-
Get database statistics
|
|
167
|
+
Get database statistics.
|
|
202
168
|
|
|
203
169
|
_No parameters required._
|
|
204
170
|
|
|
205
|
-
**Returns:** `{ memoryCount,
|
|
171
|
+
**Returns:** `{ memoryCount, tagCount, oldestMemory, newestMemory }`.
|
|
206
172
|
|
|
207
173
|
### `update_memory`
|
|
208
174
|
|
|
209
|
-
Update memory
|
|
175
|
+
Update the content of a memory. Returns the new hash since changing content changes the hash.
|
|
210
176
|
|
|
211
|
-
| Parameter
|
|
212
|
-
|
|
|
213
|
-
| `hash`
|
|
214
|
-
| `
|
|
215
|
-
| `
|
|
216
|
-
| `tags` | string[] | No | - | Replace all tags (max 100, each 1-50 chars) |
|
|
217
|
-
| `addTags` | string[] | No | - | Tags to add (max 100, each 1-50 chars) |
|
|
218
|
-
| `removeTags` | string[] | No | - | Tags to remove (max 100, each 1-50 chars) |
|
|
177
|
+
| Parameter | Type | Required | Default | Description |
|
|
178
|
+
| :-------- | :------- | :------- | :------ | :-------------------------------------- |
|
|
179
|
+
| `hash` | string | Yes | - | MD5 hash of memory to update (32 chars) |
|
|
180
|
+
| `content` | string | Yes | - | New content (1-100000 chars) |
|
|
181
|
+
| `tags` | string[] | No | - | Replace tags (max 100, each 1-50 chars) |
|
|
219
182
|
|
|
220
|
-
**Returns:** `{ updated: true,
|
|
183
|
+
**Returns:** `{ updated: true, oldHash, newHash }`.
|
|
184
|
+
|
|
185
|
+
Notes:
|
|
186
|
+
|
|
187
|
+
- If `tags` is omitted, existing tags are preserved.
|
|
188
|
+
- If `tags` is provided, it replaces all existing tags for the memory.
|
|
189
|
+
- Updating to content that already exists in another memory returns an error.
|
|
221
190
|
|
|
222
191
|
### Memory Fields
|
|
223
192
|
|
|
@@ -226,8 +195,6 @@ All memory-shaped responses include:
|
|
|
226
195
|
- `id`: integer ID
|
|
227
196
|
- `content`: original content string
|
|
228
197
|
- `summary`: optional summary (currently unset by tools)
|
|
229
|
-
- `importance`: integer 0-10
|
|
230
|
-
- `memory_type`: string
|
|
231
198
|
- `created_at`: timestamp string
|
|
232
199
|
- `accessed_at`: timestamp string
|
|
233
200
|
- `hash`: MD5 hash
|
|
@@ -283,32 +250,26 @@ Add to your `claude_desktop_config.json`:
|
|
|
283
250
|
|
|
284
251
|
## Limits & Constraints
|
|
285
252
|
|
|
286
|
-
| Constraint
|
|
287
|
-
|
|
|
288
|
-
| **Max content length**
|
|
289
|
-
| **Max query length**
|
|
290
|
-
| **Max search terms**
|
|
291
|
-
| **Max search results**
|
|
292
|
-
| **
|
|
293
|
-
| **Max
|
|
294
|
-
| **Max
|
|
295
|
-
| **
|
|
296
|
-
| **
|
|
297
|
-
| **
|
|
298
|
-
| **Max traversal depth** | 3 | Maximum depth for relationship traversal |
|
|
299
|
-
| **Importance range** | 0-10 | Allowed range for `importance` |
|
|
300
|
-
| **Min relevance range** | 0-1 | Allowed range for `minRelevance` |
|
|
301
|
-
| **Hash length** | 32 chars | MD5 hash length |
|
|
302
|
-
| **Search mode** | Tokenized OR | Whitespace-split terms are quoted and OR'ed; FTS5 operators are not supported |
|
|
253
|
+
| Constraint | Value | Description |
|
|
254
|
+
| :---------------------- | :------------ | :---------------------------------------------------------------------------- |
|
|
255
|
+
| **Max content length** | 100,000 chars | Maximum characters in memory content |
|
|
256
|
+
| **Max query length** | 1,000 chars | Maximum characters in search query |
|
|
257
|
+
| **Max search terms** | 50 | Maximum whitespace-separated terms per query |
|
|
258
|
+
| **Max search results** | 100 | Maximum results returned from `search_memories` |
|
|
259
|
+
| **Min tags per memory** | 1 | At least one tag is required |
|
|
260
|
+
| **Max tags per memory** | 100 | Maximum number of tags when storing a memory |
|
|
261
|
+
| **Max tag length** | 50 chars | Maximum characters per tag |
|
|
262
|
+
| **Tag format** | No whitespace | Tags cannot contain spaces or tabs; use hyphens for compound words |
|
|
263
|
+
| **Hash length** | 32 chars | MD5 hash length |
|
|
264
|
+
| **Search mode** | Tokenized OR | Whitespace-split terms are quoted and OR'ed; FTS5 operators are not supported |
|
|
303
265
|
|
|
304
266
|
### Notes
|
|
305
267
|
|
|
306
268
|
- **Content deduplication**: Memories are deduplicated using MD5 hashes.
|
|
307
269
|
- **Search errors**: If FTS5 is unavailable, `search_memories` returns an error indicating the index is missing. Invalid query syntax returns an error with details.
|
|
308
270
|
- **Search tokenization**: Queries are split on whitespace (max 50 terms); whitespace-only queries are rejected.
|
|
309
|
-
- **Tag
|
|
310
|
-
- **
|
|
311
|
-
- **Local storage**: All data is stored locally in `.memdb/memory.db` unless `:memory:` is used.
|
|
271
|
+
- **Tag requirements**: At least one tag is required. Tags cannot contain whitespace; use hyphens for compound words (e.g., `api-design`).
|
|
272
|
+
- **Local storage**: All data is stored locally in `.memdb/memory.db`.
|
|
312
273
|
|
|
313
274
|
## Development
|
|
314
275
|
|
|
@@ -338,28 +299,29 @@ Add to your `claude_desktop_config.json`:
|
|
|
338
299
|
|
|
339
300
|
```text
|
|
340
301
|
src/
|
|
341
|
-
|-- index.ts
|
|
342
|
-
|--
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|--
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
| |-- inputs.ts
|
|
354
|
-
| |-- outputs.ts
|
|
355
|
-
|-- lib/ # Error/response helpers
|
|
356
|
-
|-- types/ # TypeScript types
|
|
357
|
-
`-- utils/ # Config + logger
|
|
302
|
+
|-- index.ts # Server entry point (stdio transport)
|
|
303
|
+
|-- config.ts # CLI/env configuration
|
|
304
|
+
|-- logger.ts # stderr logger with level filtering
|
|
305
|
+
|-- protocol-version-guard.ts # Reject unsupported MCP protocol versions
|
|
306
|
+
|-- tools.ts # Tool registration + handlers
|
|
307
|
+
|-- schemas.ts # Zod input/output schemas
|
|
308
|
+
|-- types.ts # Shared TypeScript types
|
|
309
|
+
`-- core/ # SQLite setup + memory CRUD/search
|
|
310
|
+
|-- db.ts # DB init + schema + helpers
|
|
311
|
+
|-- memory-read.ts # Read + delete + stats
|
|
312
|
+
|-- memory-write.ts # Create + update + tag handling
|
|
313
|
+
`-- search.ts # FTS + tag search
|
|
358
314
|
|
|
359
315
|
tests/
|
|
360
316
|
`-- *.test.ts # Node.js test runner tests
|
|
361
317
|
```
|
|
362
318
|
|
|
319
|
+
## Troubleshooting
|
|
320
|
+
|
|
321
|
+
- **`node:sqlite` / `DatabaseSync` not found**: Ensure Node.js >= 22.0.0.
|
|
322
|
+
- **Search errors about FTS5**: FTS5 must be available; the server creates the
|
|
323
|
+
virtual table automatically, but your SQLite build must include FTS5 support.
|
|
324
|
+
|
|
363
325
|
## Contributing
|
|
364
326
|
|
|
365
327
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
const DEFAULT_DB_PATH = path.join(process.cwd(), '.memdb', 'memory.db');
|
|
4
|
+
const DEFAULT_LOG_LEVEL = 'info';
|
|
5
|
+
const resolveDbPath = (env) => {
|
|
6
|
+
if (env.MEMDB_PATH === ':memory:')
|
|
7
|
+
return ':memory:';
|
|
8
|
+
return path.resolve(DEFAULT_DB_PATH);
|
|
9
|
+
};
|
|
10
|
+
export const config = {
|
|
11
|
+
dbPath: resolveDbPath(process.env),
|
|
12
|
+
logLevel: DEFAULT_LOG_LEVEL,
|
|
13
|
+
};
|
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
export declare const SCHEMA_SQL = "\n PRAGMA foreign_keys = ON;\n PRAGMA journal_mode = WAL;\n PRAGMA synchronous = NORMAL;\n\n CREATE TABLE IF NOT EXISTS memories (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n content TEXT NOT NULL,\n summary TEXT,\n importance INTEGER DEFAULT 0,\n memory_type TEXT DEFAULT 'general',\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n accessed_at TEXT DEFAULT CURRENT_TIMESTAMP,\n hash TEXT UNIQUE NOT NULL\n ) STRICT;\n\n CREATE TABLE IF NOT EXISTS tags (\n memory_id INTEGER NOT NULL,\n tag TEXT NOT NULL,\n PRIMARY KEY (memory_id, tag),\n FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE\n ) STRICT;\n\n CREATE INDEX IF NOT EXISTS idx_tags_tag_memory_id ON tags(tag, memory_id);\n\n CREATE TABLE IF NOT EXISTS relationships (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n from_memory_id INTEGER NOT NULL,\n to_memory_id INTEGER NOT NULL,\n relation_type TEXT NOT NULL,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (from_memory_id) REFERENCES memories(id) ON DELETE CASCADE,\n FOREIGN KEY (to_memory_id) REFERENCES memories(id) ON DELETE CASCADE,\n UNIQUE(from_memory_id, to_memory_id, relation_type)\n ) STRICT;\n\n CREATE INDEX IF NOT EXISTS idx_relationships_to_memory_id ON relationships(to_memory_id);\n\n CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(\n content,\n summary,\n content_rowid='id'\n );\n\n CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN\n INSERT INTO memories_fts(rowid, content, summary)\n VALUES (new.id, new.content, new.summary);\n END;\n\n CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN\n DELETE FROM memories_fts WHERE rowid = old.id;\n INSERT INTO memories_fts(rowid, content, summary)\n VALUES (new.id, new.content, new.summary);\n END;\n\n CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN\n DELETE FROM memories_fts WHERE rowid = old.id;\n END;\n";
|
|
2
2
|
export declare const FTS_SYNC_SQL = "\n INSERT INTO memories_fts(rowid, content, summary)\n SELECT id, content, summary FROM memories\n WHERE id NOT IN (SELECT rowid FROM memories_fts);\n";
|
|
3
|
-
//# sourceMappingURL=database-schema.d.ts.map
|
package/dist/core/database.d.ts
CHANGED
package/dist/core/database.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DatabaseSync, type StatementSync } from 'node:sqlite';
|
|
2
|
+
import type { Memory, SearchResult } from '../types.js';
|
|
3
|
+
export type DbRow = Record<string, unknown>;
|
|
4
|
+
export declare const db: DatabaseSync;
|
|
5
|
+
export declare const closeDb: () => void;
|
|
6
|
+
export type SqlParam = string | number | bigint | null | Uint8Array;
|
|
7
|
+
export declare const prepareCached: (sql: string) => StatementSync;
|
|
8
|
+
export declare const executeAll: (stmt: StatementSync, ...params: SqlParam[]) => DbRow[];
|
|
9
|
+
export declare const executeGet: (stmt: StatementSync, ...params: SqlParam[]) => DbRow | undefined;
|
|
10
|
+
export declare const executeRun: (stmt: StatementSync, ...params: SqlParam[]) => {
|
|
11
|
+
changes: number | bigint;
|
|
12
|
+
};
|
|
13
|
+
export declare const withImmediateTransaction: <T>(operation: () => T) => T;
|
|
14
|
+
export declare const toSafeInteger: (value: unknown, field: string) => number;
|
|
15
|
+
export declare const mapRowToMemory: (row: DbRow) => Memory;
|
|
16
|
+
export declare const mapRowToSearchResult: (row: DbRow) => SearchResult;
|
package/dist/core/db.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
const SCHEMA_SQL = `
|
|
6
|
+
PRAGMA foreign_keys = ON;
|
|
7
|
+
PRAGMA journal_mode = WAL;
|
|
8
|
+
PRAGMA synchronous = NORMAL;
|
|
9
|
+
|
|
10
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
11
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
|
+
content TEXT NOT NULL,
|
|
13
|
+
summary TEXT,
|
|
14
|
+
importance INTEGER DEFAULT 0,
|
|
15
|
+
memory_type TEXT DEFAULT 'general',
|
|
16
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
17
|
+
accessed_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
18
|
+
hash TEXT UNIQUE NOT NULL
|
|
19
|
+
) STRICT;
|
|
20
|
+
|
|
21
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
22
|
+
memory_id INTEGER NOT NULL,
|
|
23
|
+
tag TEXT NOT NULL,
|
|
24
|
+
PRIMARY KEY (memory_id, tag),
|
|
25
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
26
|
+
) STRICT;
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_tags_tag_memory_id ON tags(tag, memory_id);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS relationships (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
from_memory_id INTEGER NOT NULL,
|
|
33
|
+
to_memory_id INTEGER NOT NULL,
|
|
34
|
+
relation_type TEXT NOT NULL,
|
|
35
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
36
|
+
FOREIGN KEY (from_memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
37
|
+
FOREIGN KEY (to_memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
38
|
+
UNIQUE(from_memory_id, to_memory_id, relation_type)
|
|
39
|
+
) STRICT;
|
|
40
|
+
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_to_memory_id ON relationships(to_memory_id);
|
|
42
|
+
|
|
43
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
44
|
+
content,
|
|
45
|
+
summary,
|
|
46
|
+
content_rowid='id'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
50
|
+
INSERT INTO memories_fts(rowid, content, summary)
|
|
51
|
+
VALUES (new.id, new.content, new.summary);
|
|
52
|
+
END;
|
|
53
|
+
|
|
54
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
55
|
+
DELETE FROM memories_fts WHERE rowid = old.id;
|
|
56
|
+
INSERT INTO memories_fts(rowid, content, summary)
|
|
57
|
+
VALUES (new.id, new.content, new.summary);
|
|
58
|
+
END;
|
|
59
|
+
|
|
60
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
61
|
+
DELETE FROM memories_fts WHERE rowid = old.id;
|
|
62
|
+
END;
|
|
63
|
+
`;
|
|
64
|
+
const FTS_SYNC_SQL = `
|
|
65
|
+
INSERT INTO memories_fts(rowid, content, summary)
|
|
66
|
+
SELECT id, content, summary FROM memories
|
|
67
|
+
WHERE id NOT IN (SELECT rowid FROM memories_fts);
|
|
68
|
+
`;
|
|
69
|
+
const ensureDbDirectory = async (dbPath) => {
|
|
70
|
+
if (dbPath === ':memory:')
|
|
71
|
+
return;
|
|
72
|
+
await mkdir(path.dirname(dbPath), { recursive: true });
|
|
73
|
+
};
|
|
74
|
+
const isEnableDefensive = (value) => {
|
|
75
|
+
return typeof value === 'function';
|
|
76
|
+
};
|
|
77
|
+
const enableDefensiveMode = (database) => {
|
|
78
|
+
const enableDefensive = Reflect.get(database, 'enableDefensive');
|
|
79
|
+
if (!isEnableDefensive(enableDefensive))
|
|
80
|
+
return;
|
|
81
|
+
enableDefensive(true);
|
|
82
|
+
};
|
|
83
|
+
const isInTransaction = (database) => {
|
|
84
|
+
const prop = Reflect.get(database, 'isTransaction');
|
|
85
|
+
return typeof prop === 'boolean' ? prop : false;
|
|
86
|
+
};
|
|
87
|
+
const initializeSchema = (database) => {
|
|
88
|
+
database.exec(SCHEMA_SQL);
|
|
89
|
+
database.exec(FTS_SYNC_SQL);
|
|
90
|
+
};
|
|
91
|
+
const createDatabase = (dbPath) => {
|
|
92
|
+
const database = new DatabaseSync(dbPath, {
|
|
93
|
+
timeout: 5000,
|
|
94
|
+
enableForeignKeyConstraints: true,
|
|
95
|
+
allowExtension: false,
|
|
96
|
+
});
|
|
97
|
+
enableDefensiveMode(database);
|
|
98
|
+
initializeSchema(database);
|
|
99
|
+
return database;
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
await ensureDbDirectory(config.dbPath);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
console.error(`[ERROR] Failed to create database directory: ${message}`);
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
export const db = createDatabase(config.dbPath);
|
|
110
|
+
export const closeDb = () => {
|
|
111
|
+
if (!db.isOpen)
|
|
112
|
+
return;
|
|
113
|
+
db.close();
|
|
114
|
+
};
|
|
115
|
+
const MAX_CACHED_STATEMENTS = 200;
|
|
116
|
+
const statementCache = new Map();
|
|
117
|
+
const statementCacheOrder = [];
|
|
118
|
+
const enforceStatementCacheLimit = () => {
|
|
119
|
+
if (statementCacheOrder.length <= MAX_CACHED_STATEMENTS)
|
|
120
|
+
return;
|
|
121
|
+
const oldestSql = statementCacheOrder.shift();
|
|
122
|
+
if (!oldestSql)
|
|
123
|
+
return;
|
|
124
|
+
statementCache.delete(oldestSql);
|
|
125
|
+
};
|
|
126
|
+
const isDbRow = (value) => {
|
|
127
|
+
return typeof value === 'object' && value !== null;
|
|
128
|
+
};
|
|
129
|
+
const toDbRowArray = (value) => {
|
|
130
|
+
if (!Array.isArray(value)) {
|
|
131
|
+
throw new Error('Expected rows array');
|
|
132
|
+
}
|
|
133
|
+
const rows = [];
|
|
134
|
+
for (const row of value) {
|
|
135
|
+
if (!isDbRow(row)) {
|
|
136
|
+
throw new Error('Invalid row');
|
|
137
|
+
}
|
|
138
|
+
rows.push(row);
|
|
139
|
+
}
|
|
140
|
+
return rows;
|
|
141
|
+
};
|
|
142
|
+
const toDbRowOrUndefined = (value) => {
|
|
143
|
+
if (value === undefined)
|
|
144
|
+
return undefined;
|
|
145
|
+
if (!isDbRow(value)) {
|
|
146
|
+
throw new Error('Invalid row');
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
};
|
|
150
|
+
const toRunResult = (value) => {
|
|
151
|
+
if (typeof value !== 'object' || value === null) {
|
|
152
|
+
throw new Error('Invalid run result');
|
|
153
|
+
}
|
|
154
|
+
const changes = Reflect.get(value, 'changes');
|
|
155
|
+
if (typeof changes !== 'number' && typeof changes !== 'bigint') {
|
|
156
|
+
throw new Error('Invalid run result');
|
|
157
|
+
}
|
|
158
|
+
return { changes };
|
|
159
|
+
};
|
|
160
|
+
export const prepareCached = (sql) => {
|
|
161
|
+
const cached = statementCache.get(sql);
|
|
162
|
+
if (cached)
|
|
163
|
+
return cached;
|
|
164
|
+
const stmt = db.prepare(sql);
|
|
165
|
+
statementCache.set(sql, stmt);
|
|
166
|
+
statementCacheOrder.push(sql);
|
|
167
|
+
enforceStatementCacheLimit();
|
|
168
|
+
return stmt;
|
|
169
|
+
};
|
|
170
|
+
export const executeAll = (stmt, ...params) => toDbRowArray(stmt.all(...params));
|
|
171
|
+
export const executeGet = (stmt, ...params) => toDbRowOrUndefined(stmt.get(...params));
|
|
172
|
+
export const executeRun = (stmt, ...params) => toRunResult(stmt.run(...params));
|
|
173
|
+
export const withImmediateTransaction = (operation) => {
|
|
174
|
+
if (isInTransaction(db)) {
|
|
175
|
+
throw new Error('Cannot start nested transaction');
|
|
176
|
+
}
|
|
177
|
+
db.exec('BEGIN IMMEDIATE');
|
|
178
|
+
try {
|
|
179
|
+
const result = operation();
|
|
180
|
+
db.exec('COMMIT');
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
db.exec('ROLLBACK');
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const createFieldError = (field) => new Error(`Invalid ${field}`);
|
|
189
|
+
const toNumber = (value, field) => {
|
|
190
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
191
|
+
return value;
|
|
192
|
+
if (typeof value === 'bigint') {
|
|
193
|
+
const numeric = Number(value);
|
|
194
|
+
if (Number.isFinite(numeric))
|
|
195
|
+
return numeric;
|
|
196
|
+
}
|
|
197
|
+
throw createFieldError(field);
|
|
198
|
+
};
|
|
199
|
+
export const toSafeInteger = (value, field) => {
|
|
200
|
+
const numeric = toNumber(value, field);
|
|
201
|
+
if (!Number.isSafeInteger(numeric)) {
|
|
202
|
+
throw createFieldError(field);
|
|
203
|
+
}
|
|
204
|
+
return numeric;
|
|
205
|
+
};
|
|
206
|
+
const toString = (value, field) => {
|
|
207
|
+
if (typeof value === 'string')
|
|
208
|
+
return value;
|
|
209
|
+
throw createFieldError(field);
|
|
210
|
+
};
|
|
211
|
+
const toOptionalString = (value, field) => {
|
|
212
|
+
if (value === null || value === undefined)
|
|
213
|
+
return undefined;
|
|
214
|
+
return toString(value, field);
|
|
215
|
+
};
|
|
216
|
+
const toOptionalNumber = (value, field) => {
|
|
217
|
+
if (value === null || value === undefined)
|
|
218
|
+
return undefined;
|
|
219
|
+
return toNumber(value, field);
|
|
220
|
+
};
|
|
221
|
+
export const mapRowToMemory = (row) => ({
|
|
222
|
+
id: toSafeInteger(row.id, 'id'),
|
|
223
|
+
content: toString(row.content, 'content'),
|
|
224
|
+
summary: toOptionalString(row.summary, 'summary'),
|
|
225
|
+
created_at: toString(row.created_at, 'created_at'),
|
|
226
|
+
accessed_at: toString(row.accessed_at, 'accessed_at'),
|
|
227
|
+
hash: toString(row.hash, 'hash'),
|
|
228
|
+
});
|
|
229
|
+
export const mapRowToSearchResult = (row) => ({
|
|
230
|
+
...mapRowToMemory(row),
|
|
231
|
+
relevance: toOptionalNumber(row.relevance, 'relevance') ?? 0,
|
|
232
|
+
});
|
|
@@ -4,24 +4,31 @@ import { findMemoryIdByHash, insertTags } from './memory-db.js';
|
|
|
4
4
|
import { toSafeInteger } from './row-mappers.js';
|
|
5
5
|
import { executeGet, withImmediateTransaction } from './sqlite.js';
|
|
6
6
|
import { normalizeTags } from './tags.js';
|
|
7
|
-
const
|
|
7
|
+
const MAX_TAGS = 100;
|
|
8
|
+
const buildHash = (content) => {
|
|
9
|
+
// eslint-disable-next-line sonarjs/hashing -- MD5 used for non-security deduplication only.
|
|
10
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
11
|
+
};
|
|
8
12
|
const stmtInsertMemory = db.prepare('INSERT OR IGNORE INTO memories (content, importance, memory_type, hash) ' +
|
|
9
13
|
'VALUES (?, ?, ?, ?) RETURNING id');
|
|
14
|
+
const requireMemoryId = (id) => {
|
|
15
|
+
if (id === undefined) {
|
|
16
|
+
throw new Error('Failed to resolve memory id');
|
|
17
|
+
}
|
|
18
|
+
return id;
|
|
19
|
+
};
|
|
10
20
|
const resolveMemoryId = (input) => {
|
|
11
21
|
const inserted = executeGet(stmtInsertMemory, input.content, input.importance, input.memoryType, input.hash);
|
|
12
22
|
if (inserted) {
|
|
13
23
|
return { id: toSafeInteger(inserted.id, 'id'), isNew: true };
|
|
14
24
|
}
|
|
15
|
-
const id = findMemoryIdByHash(input.hash);
|
|
16
|
-
if (id === undefined) {
|
|
17
|
-
throw new Error('Failed to resolve memory id');
|
|
18
|
-
}
|
|
25
|
+
const id = requireMemoryId(findMemoryIdByHash(input.hash));
|
|
19
26
|
return { id, isNew: false };
|
|
20
27
|
};
|
|
21
28
|
export const createMemory = (input) => withImmediateTransaction(() => {
|
|
22
29
|
const { content, tags = [], importance = 0, memoryType = 'general', } = input;
|
|
23
30
|
const hash = buildHash(content);
|
|
24
|
-
const normalizedTags = normalizeTags(tags,
|
|
31
|
+
const normalizedTags = normalizeTags(tags, MAX_TAGS);
|
|
25
32
|
const { id, isNew } = resolveMemoryId({
|
|
26
33
|
content,
|
|
27
34
|
importance,
|
|
@@ -31,4 +38,3 @@ export const createMemory = (input) => withImmediateTransaction(() => {
|
|
|
31
38
|
insertTags(id, normalizedTags);
|
|
32
39
|
return { id, hash, isNew };
|
|
33
40
|
});
|
|
34
|
-
//# sourceMappingURL=memory-create.js.map
|
package/dist/core/memory-db.d.ts
CHANGED
package/dist/core/memory-db.js
CHANGED