@j0hanz/memdb 1.0.10 → 1.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.
Files changed (87) hide show
  1. package/README.md +82 -120
  2. package/dist/config.d.ts +4 -0
  3. package/dist/config.js +13 -0
  4. package/dist/core/database-schema.d.ts +0 -1
  5. package/dist/core/database-schema.js +0 -1
  6. package/dist/core/database.d.ts +0 -1
  7. package/dist/core/database.js +0 -1
  8. package/dist/core/db.d.ts +16 -0
  9. package/dist/core/db.js +225 -0
  10. package/dist/core/memory-create.d.ts +0 -1
  11. package/dist/core/memory-create.js +13 -7
  12. package/dist/core/memory-db.d.ts +0 -1
  13. package/dist/core/memory-db.js +0 -1
  14. package/dist/core/memory-read.d.ts +2 -2
  15. package/dist/core/memory-read.js +32 -4
  16. package/dist/core/memory-relations.d.ts +0 -1
  17. package/dist/core/memory-relations.js +0 -1
  18. package/dist/core/memory-search.d.ts +8 -7
  19. package/dist/core/memory-search.js +15 -9
  20. package/dist/core/memory-stats.d.ts +0 -1
  21. package/dist/core/memory-stats.js +0 -1
  22. package/dist/core/memory-updates.d.ts +0 -1
  23. package/dist/core/memory-updates.js +1 -2
  24. package/dist/core/memory-write.d.ts +13 -0
  25. package/dist/core/memory-write.js +113 -0
  26. package/dist/core/relation-queries.d.ts +0 -1
  27. package/dist/core/relation-queries.js +0 -1
  28. package/dist/core/relations.d.ts +10 -0
  29. package/dist/core/relations.js +177 -0
  30. package/dist/core/row-mappers.d.ts +0 -1
  31. package/dist/core/row-mappers.js +0 -1
  32. package/dist/core/search-errors.d.ts +0 -1
  33. package/dist/core/search-errors.js +0 -1
  34. package/dist/core/search.d.ts +5 -12
  35. package/dist/core/search.js +77 -56
  36. package/dist/core/sqlite.d.ts +0 -1
  37. package/dist/core/sqlite.js +0 -3
  38. package/dist/core/tags.d.ts +0 -1
  39. package/dist/core/tags.js +1 -2
  40. package/dist/index.d.ts +0 -1
  41. package/dist/index.js +43 -37
  42. package/dist/lib/errors.d.ts +0 -1
  43. package/dist/lib/errors.js +0 -1
  44. package/dist/logger.d.ts +5 -0
  45. package/dist/logger.js +17 -0
  46. package/dist/protocol-version-guard.d.ts +15 -0
  47. package/dist/protocol-version-guard.js +65 -0
  48. package/dist/schemas/inputs.d.ts +0 -1
  49. package/dist/schemas/inputs.js +0 -1
  50. package/dist/schemas/outputs.d.ts +0 -1
  51. package/dist/schemas/outputs.js +0 -1
  52. package/dist/schemas.d.ts +28 -0
  53. package/dist/schemas.js +65 -0
  54. package/dist/tools/definitions/memory-core.d.ts +0 -1
  55. package/dist/tools/definitions/memory-core.js +0 -1
  56. package/dist/tools/definitions/memory-relations.d.ts +0 -1
  57. package/dist/tools/definitions/memory-relations.js +0 -1
  58. package/dist/tools/definitions/memory-search.d.ts +0 -1
  59. package/dist/tools/definitions/memory-search.js +1 -11
  60. package/dist/tools/definitions/memory-stats.d.ts +0 -1
  61. package/dist/tools/definitions/memory-stats.js +0 -1
  62. package/dist/tools/index.d.ts +0 -1
  63. package/dist/tools/index.js +0 -1
  64. package/dist/tools/tool-handlers.d.ts +0 -1
  65. package/dist/tools/tool-handlers.js +0 -1
  66. package/dist/tools/tool-types.d.ts +0 -1
  67. package/dist/tools/tool-types.js +0 -1
  68. package/dist/tools.d.ts +18 -0
  69. package/dist/tools.js +167 -0
  70. package/dist/tsconfig.tsbuildinfo +1 -0
  71. package/dist/types/index.d.ts +0 -1
  72. package/dist/types/index.js +0 -1
  73. package/dist/types.d.ts +30 -0
  74. package/dist/types.js +1 -0
  75. package/dist/utils/config.d.ts +0 -1
  76. package/dist/utils/config.js +0 -1
  77. package/dist/utils/logger.d.ts +0 -1
  78. package/dist/utils/logger.js +0 -1
  79. package/dist/utils.d.ts +11 -0
  80. package/dist/utils.js +118 -0
  81. package/dist/worker/db-worker-client.d.ts +9 -0
  82. package/dist/worker/db-worker-client.js +93 -0
  83. package/dist/worker/db-worker.d.ts +1 -0
  84. package/dist/worker/db-worker.js +174 -0
  85. package/dist/worker/protocol.d.ts +9 -0
  86. package/dist/worker/protocol.js +14 -0
  87. package/package.json +9 -6
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # memdb
2
2
 
3
- A SQLite-backed MCP memory server (on-disk by default, in-memory optional).
3
+ A SQLite-backed MCP memory server with local workspace storage.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@j0hanz/memdb.svg)](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 | Description |
16
- | :---------------- | :---------------------------------------------------------------- |
17
- | Memory Storage | Store text memories with tags, importance, and type |
18
- | Full-Text Search | FTS5-backed tokenized search with relevance ranking |
19
- | Graph Connections | Link memories and traverse relationships |
20
- | Stats | Memory, tag, and relationship counts + activity range |
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
- ## Configuration
60
+ ## Storage
62
61
 
63
- The server uses a local SQLite database at `<cwd>/.memdb/memory.db` by default.
64
- The path is resolved to an absolute path unless you use `:memory:`.
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
- ### Environment Variables
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
- - `MEMDB_PATH`: Override the database path (`:memory:` for in-memory).
69
- - `MEMDB_LOG_LEVEL`: `info`, `warn`, or `error` (default: `info`).
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 optional tags and metadata.
115
+ Store a new memory with tags.
121
116
 
122
- | Parameter | Type | Required | Default | Description |
123
- | :----------- | :------- | :------- | :-------- | :----------------------------------------- |
124
- | `content` | string | Yes | - | The content of the memory (1-100000 chars) |
125
- | `tags` | string[] | No | - | Tags (max 100, each 1-50 chars) |
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
- Full-text search with filters.
131
+ Search memories by content and tags.
138
132
 
139
- | Parameter | Type | Required | Default | Description |
140
- | :------------- | :------- | :------- | :------ | :-------------------------------- |
141
- | `query` | string | Yes | - | Search query (1-1000 chars) |
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 and memory type breakdown.
167
+ Get database statistics.
202
168
 
203
169
  _No parameters required._
204
170
 
205
- **Returns:** `{ memoryCount, relationshipCount, tagCount, memoryTypes, oldestMemory, newestMemory }`.
171
+ **Returns:** `{ memoryCount, tagCount, oldestMemory, newestMemory }`.
206
172
 
207
173
  ### `update_memory`
208
174
 
209
- Update memory metadata (content cannot be changed).
175
+ Update the content of a memory. Returns the new hash since changing content changes the hash.
210
176
 
211
- | Parameter | Type | Required | Default | Description |
212
- | :----------- | :------- | :------- | :------ | :------------------------------------------ |
213
- | `hash` | string | Yes | - | MD5 hash (32 chars) |
214
- | `importance` | number | No | - | New importance score (0-10) |
215
- | `memoryType` | string | No | - | New memory type (1-50 chars) |
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, hash }`.
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 | Value | Description |
287
- | :---------------------------- | :------------ | :---------------------------------------------------------------------------- |
288
- | **Max content length** | 100,000 chars | Maximum characters in memory content |
289
- | **Max query length** | 1,000 chars | Maximum characters in search query |
290
- | **Max search terms** | 50 | Maximum whitespace-separated terms per query |
291
- | **Max search results** | 100 | Maximum results returned from `search_memories` |
292
- | **Default search limit** | 10 | Default `limit` for `search_memories` |
293
- | **Max search offset** | 1,000 | Maximum `offset` for `search_memories` |
294
- | **Max tags per memory** | 100 | Maximum number of tags when storing a memory |
295
- | **Max tag length** | 50 chars | Maximum characters per tag |
296
- | **Max tags in search filter** | 50 | Maximum tags when filtering search results |
297
- | **Max related memories** | 1,000 | Maximum results from `get_related` queries |
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 behavior**: Tags are de-duplicated per memory; exceeding tag limits throws an error.
310
- - **Bidirectional depth**: `get_related` with `direction: "both"` caps traversal depth at 2.
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 # Server entry point (stdio transport)
342
- |-- core/ # SQLite setup + memory CRUD/search/relations
343
- | |-- database.ts # DB init + schema sync
344
- | |-- memory-create.ts
345
- | |-- memory-read.ts
346
- | |-- memory-search.ts
347
- | |-- memory-relations.ts
348
- | |-- memory-updates.ts
349
- | |-- memory-stats.ts
350
- |-- tools/ # Tool registration + handlers
351
- | |-- definitions/ # Tool metadata + handlers
352
- |-- schemas/ # Zod input/output schemas
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.
@@ -0,0 +1,4 @@
1
+ export declare const config: {
2
+ dbPath: string;
3
+ logLevel: "info";
4
+ };
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
@@ -62,4 +62,3 @@ export const FTS_SYNC_SQL = `
62
62
  SELECT id, content, summary FROM memories
63
63
  WHERE id NOT IN (SELECT rowid FROM memories_fts);
64
64
  `;
65
- //# sourceMappingURL=database-schema.js.map
@@ -1,4 +1,3 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  export declare const db: DatabaseSync;
3
3
  export declare const closeDb: () => void;
4
- //# sourceMappingURL=database.d.ts.map
@@ -41,4 +41,3 @@ export const closeDb = () => {
41
41
  return;
42
42
  db.close();
43
43
  };
44
- //# sourceMappingURL=database.js.map
@@ -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;
@@ -0,0 +1,225 @@
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 initializeSchema = (database) => {
84
+ database.exec(SCHEMA_SQL);
85
+ database.exec(FTS_SYNC_SQL);
86
+ };
87
+ const createDatabase = (dbPath) => {
88
+ const database = new DatabaseSync(dbPath, {
89
+ timeout: 5000,
90
+ enableForeignKeyConstraints: true,
91
+ allowExtension: false,
92
+ });
93
+ enableDefensiveMode(database);
94
+ initializeSchema(database);
95
+ return database;
96
+ };
97
+ try {
98
+ await ensureDbDirectory(config.dbPath);
99
+ }
100
+ catch (err) {
101
+ const message = err instanceof Error ? err.message : String(err);
102
+ console.error(`[ERROR] Failed to create database directory: ${message}`);
103
+ throw err;
104
+ }
105
+ export const db = createDatabase(config.dbPath);
106
+ export const closeDb = () => {
107
+ if (!db.isOpen)
108
+ return;
109
+ db.close();
110
+ };
111
+ const MAX_CACHED_STATEMENTS = 200;
112
+ const statementCache = new Map();
113
+ const statementCacheOrder = [];
114
+ const enforceStatementCacheLimit = () => {
115
+ if (statementCacheOrder.length <= MAX_CACHED_STATEMENTS)
116
+ return;
117
+ const oldestSql = statementCacheOrder.shift();
118
+ if (!oldestSql)
119
+ return;
120
+ statementCache.delete(oldestSql);
121
+ };
122
+ const isDbRow = (value) => {
123
+ return typeof value === 'object' && value !== null;
124
+ };
125
+ const toDbRowArray = (value) => {
126
+ if (!Array.isArray(value)) {
127
+ throw new Error('Expected rows array');
128
+ }
129
+ const rows = [];
130
+ for (const row of value) {
131
+ if (!isDbRow(row)) {
132
+ throw new Error('Invalid row');
133
+ }
134
+ rows.push(row);
135
+ }
136
+ return rows;
137
+ };
138
+ const toDbRowOrUndefined = (value) => {
139
+ if (value === undefined)
140
+ return undefined;
141
+ if (!isDbRow(value)) {
142
+ throw new Error('Invalid row');
143
+ }
144
+ return value;
145
+ };
146
+ const toRunResult = (value) => {
147
+ if (typeof value !== 'object' || value === null) {
148
+ throw new Error('Invalid run result');
149
+ }
150
+ const changes = Reflect.get(value, 'changes');
151
+ if (typeof changes !== 'number' && typeof changes !== 'bigint') {
152
+ throw new Error('Invalid run result');
153
+ }
154
+ return { changes };
155
+ };
156
+ export const prepareCached = (sql) => {
157
+ const cached = statementCache.get(sql);
158
+ if (cached)
159
+ return cached;
160
+ const stmt = db.prepare(sql);
161
+ statementCache.set(sql, stmt);
162
+ statementCacheOrder.push(sql);
163
+ enforceStatementCacheLimit();
164
+ return stmt;
165
+ };
166
+ export const executeAll = (stmt, ...params) => toDbRowArray(stmt.all(...params));
167
+ export const executeGet = (stmt, ...params) => toDbRowOrUndefined(stmt.get(...params));
168
+ export const executeRun = (stmt, ...params) => toRunResult(stmt.run(...params));
169
+ export const withImmediateTransaction = (operation) => {
170
+ db.exec('BEGIN IMMEDIATE');
171
+ try {
172
+ const result = operation();
173
+ db.exec('COMMIT');
174
+ return result;
175
+ }
176
+ catch (err) {
177
+ db.exec('ROLLBACK');
178
+ throw err;
179
+ }
180
+ };
181
+ const createFieldError = (field) => new Error(`Invalid ${field}`);
182
+ const toNumber = (value, field) => {
183
+ if (typeof value === 'number' && Number.isFinite(value))
184
+ return value;
185
+ if (typeof value === 'bigint') {
186
+ const numeric = Number(value);
187
+ if (Number.isFinite(numeric))
188
+ return numeric;
189
+ }
190
+ throw createFieldError(field);
191
+ };
192
+ export const toSafeInteger = (value, field) => {
193
+ const numeric = toNumber(value, field);
194
+ if (!Number.isSafeInteger(numeric)) {
195
+ throw createFieldError(field);
196
+ }
197
+ return numeric;
198
+ };
199
+ const toString = (value, field) => {
200
+ if (typeof value === 'string')
201
+ return value;
202
+ throw createFieldError(field);
203
+ };
204
+ const toOptionalString = (value, field) => {
205
+ if (value === null || value === undefined)
206
+ return undefined;
207
+ return toString(value, field);
208
+ };
209
+ const toOptionalNumber = (value, field) => {
210
+ if (value === null || value === undefined)
211
+ return undefined;
212
+ return toNumber(value, field);
213
+ };
214
+ export const mapRowToMemory = (row) => ({
215
+ id: toSafeInteger(row.id, 'id'),
216
+ content: toString(row.content, 'content'),
217
+ summary: toOptionalString(row.summary, 'summary'),
218
+ created_at: toString(row.created_at, 'created_at'),
219
+ accessed_at: toString(row.accessed_at, 'accessed_at'),
220
+ hash: toString(row.hash, 'hash'),
221
+ });
222
+ export const mapRowToSearchResult = (row) => ({
223
+ ...mapRowToMemory(row),
224
+ relevance: toOptionalNumber(row.relevance, 'relevance') ?? 0,
225
+ });
@@ -5,4 +5,3 @@ export declare const createMemory: (input: {
5
5
  importance?: number;
6
6
  memoryType?: string;
7
7
  }) => MemoryInsertResult;
8
- //# sourceMappingURL=memory-create.d.ts.map
@@ -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 buildHash = (content) => crypto.createHash('md5').update(content).digest('hex');
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, 100);
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
@@ -1,3 +1,2 @@
1
1
  export declare const findMemoryIdByHash: (hash: string) => number | undefined;
2
2
  export declare const insertTags: (memoryId: number, tags: readonly string[]) => void;
3
- //# sourceMappingURL=memory-db.d.ts.map
@@ -29,4 +29,3 @@ export const insertTags = (memoryId, tags) => {
29
29
  const stmt = getInsertTagsStatement(tags.length);
30
30
  executeRun(stmt, ...params);
31
31
  };
32
- //# sourceMappingURL=memory-db.js.map
@@ -1,4 +1,4 @@
1
- import type { Memory, StatementResult } from '../types/index.js';
1
+ import type { Memory, MemoryStats, StatementResult } from '../types.js';
2
2
  export declare const getMemory: (hash: string) => Memory | undefined;
3
3
  export declare const deleteMemory: (hash: string) => StatementResult;
4
- //# sourceMappingURL=memory-read.d.ts.map
4
+ export declare const getStats: () => MemoryStats;