@j0hanz/memdb 1.2.5 → 1.2.6

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 CHANGED
@@ -161,7 +161,7 @@ Search memories by content and tags.
161
161
  | :-------- | :----- | :------- | :------ | :---------------------------------------- |
162
162
  | `query` | string | Yes | - | Search query (1-1000 chars, max 50 terms) |
163
163
 
164
- **Returns:** Array of search results (`Memory` + `relevance`).
164
+ **Returns:** Array of search results (`Memory` + `relevance`, includes `tags`).
165
165
 
166
166
  Notes:
167
167
 
@@ -177,7 +177,11 @@ Retrieve a specific memory by its hash.
177
177
  | :-------- | :----- | :------- | :------ | :------------------ |
178
178
  | `hash` | string | Yes | - | MD5 hash (32 chars) |
179
179
 
180
- **Returns:** `Memory`.
180
+ **Returns:** `Memory` (includes `tags`).
181
+
182
+ Notes:
183
+
184
+ - Updates `accessed_at` on read.
181
185
 
182
186
  ### `delete_memory`
183
187
 
@@ -242,6 +246,7 @@ All memory-shaped responses include:
242
246
  - `id`: integer ID
243
247
  - `content`: original content string
244
248
  - `summary`: optional summary (currently unset by tools)
249
+ - `tags`: string array
245
250
  - `created_at`: timestamp string
246
251
  - `accessed_at`: timestamp string
247
252
  - `hash`: MD5 hash
package/dist/core/db.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DatabaseSync, type StatementSync } from 'node:sqlite';
2
- import { type Memory, type SearchResult } from '../types.js';
2
+ import { type Memory, type Relationship, type SearchResult } from '../types.js';
3
3
  export type DbRow = Record<string, unknown>;
4
4
  export declare const db: DatabaseSync;
5
5
  export declare const closeDb: () => void;
@@ -12,5 +12,7 @@ export declare const executeRun: (stmt: StatementSync, ...params: SqlParam[]) =>
12
12
  };
13
13
  export declare const withImmediateTransaction: <T>(operation: () => T) => T;
14
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;
15
+ export declare const mapRowToMemory: (row: DbRow, tags?: string[]) => Memory;
16
+ export declare const mapRowToSearchResult: (row: DbRow, tags?: string[]) => SearchResult;
17
+ export declare const mapRowToRelationship: (row: DbRow) => Relationship;
18
+ export declare const loadTagsForMemoryIds: (memoryIds: readonly number[]) => Map<number, string[]>;
package/dist/core/db.js CHANGED
@@ -67,23 +67,18 @@ const FTS_SYNC_SQL = `
67
67
  WHERE id NOT IN (SELECT rowid FROM memories_fts);
68
68
  `;
69
69
  const withTimeout = async (promise, ms, message) => {
70
- const controller = new AbortController();
71
- const timeout = setTimeout(() => {
72
- controller.abort();
73
- }, ms);
70
+ let timeout;
71
+ const timeoutPromise = new Promise((_, reject) => {
72
+ timeout = setTimeout(() => {
73
+ reject(new Error(message));
74
+ }, ms);
75
+ });
74
76
  try {
75
- const result = await Promise.race([
76
- promise,
77
- new Promise((_, reject) => {
78
- controller.signal.addEventListener('abort', () => {
79
- reject(new Error(message));
80
- });
81
- }),
82
- ]);
83
- return result;
77
+ return await Promise.race([promise, timeoutPromise]);
84
78
  }
85
79
  finally {
86
- clearTimeout(timeout);
80
+ if (timeout)
81
+ clearTimeout(timeout);
87
82
  }
88
83
  };
89
84
  const ensureDbDirectory = async (dbPath) => {
@@ -146,26 +141,22 @@ const enforceStatementCacheLimit = () => {
146
141
  const isDbRow = (value) => {
147
142
  return typeof value === 'object' && value !== null;
148
143
  };
144
+ const assertDbRow = (value) => {
145
+ if (!isDbRow(value)) {
146
+ throw new Error('Invalid row');
147
+ }
148
+ return value;
149
+ };
149
150
  const toDbRowArray = (value) => {
150
151
  if (!Array.isArray(value)) {
151
152
  throw new Error('Expected rows array');
152
153
  }
153
- const rows = [];
154
- for (const row of value) {
155
- if (!isDbRow(row)) {
156
- throw new Error('Invalid row');
157
- }
158
- rows.push(row);
159
- }
160
- return rows;
154
+ return value.map(assertDbRow);
161
155
  };
162
156
  const toDbRowOrUndefined = (value) => {
163
157
  if (value === undefined)
164
158
  return undefined;
165
- if (!isDbRow(value)) {
166
- throw new Error('Invalid row');
167
- }
168
- return value;
159
+ return assertDbRow(value);
169
160
  };
170
161
  const toRunResult = (value) => {
171
162
  if (typeof value !== 'object' || value === null) {
@@ -206,13 +197,18 @@ export const withImmediateTransaction = (operation) => {
206
197
  }
207
198
  };
208
199
  const createFieldError = (field) => new Error(`Invalid ${field}`);
200
+ const assertFiniteNumber = (value, field) => {
201
+ if (!Number.isFinite(value)) {
202
+ throw createFieldError(field);
203
+ }
204
+ return value;
205
+ };
209
206
  const toNumber = (value, field) => {
210
- if (typeof value === 'number' && Number.isFinite(value))
211
- return value;
207
+ if (typeof value === 'number') {
208
+ return assertFiniteNumber(value, field);
209
+ }
212
210
  if (typeof value === 'bigint') {
213
- const numeric = Number(value);
214
- if (Number.isFinite(numeric))
215
- return numeric;
211
+ return assertFiniteNumber(Number(value), field);
216
212
  }
217
213
  throw createFieldError(field);
218
214
  };
@@ -246,17 +242,68 @@ const toOptionalNumber = (value, field) => {
246
242
  return undefined;
247
243
  return toNumber(value, field);
248
244
  };
249
- export const mapRowToMemory = (row) => ({
245
+ export const mapRowToMemory = (row, tags = []) => ({
250
246
  id: toSafeInteger(row.id, 'id'),
251
247
  content: toString(row.content, 'content'),
252
248
  summary: toOptionalString(row.summary, 'summary'),
249
+ tags,
253
250
  importance: toSafeInteger(row.importance ?? 0, 'importance'),
254
251
  memory_type: toMemoryType(row.memory_type ?? 'general', 'memory_type'),
255
252
  created_at: toString(row.created_at, 'created_at'),
256
253
  accessed_at: toString(row.accessed_at, 'accessed_at'),
257
254
  hash: toString(row.hash, 'hash'),
258
255
  });
259
- export const mapRowToSearchResult = (row) => ({
260
- ...mapRowToMemory(row),
256
+ export const mapRowToSearchResult = (row, tags = []) => ({
257
+ ...mapRowToMemory(row, tags),
261
258
  relevance: toOptionalNumber(row.relevance, 'relevance') ?? 0,
262
259
  });
260
+ export const mapRowToRelationship = (row) => ({
261
+ id: toSafeInteger(row.id, 'id'),
262
+ from_hash: toString(row.from_hash, 'from_hash'),
263
+ to_hash: toString(row.to_hash, 'to_hash'),
264
+ relation_type: toString(row.relation_type, 'relation_type'),
265
+ created_at: toString(row.created_at, 'created_at'),
266
+ });
267
+ const tagsSelectStatements = [];
268
+ const getSelectTagsStatement = (idCount) => {
269
+ const cached = tagsSelectStatements[idCount];
270
+ if (cached)
271
+ return cached;
272
+ const placeholders = Array.from({ length: idCount }, () => '?').join(', ');
273
+ const stmt = db.prepare(`SELECT memory_id, tag FROM tags WHERE memory_id IN (${placeholders}) ORDER BY memory_id, tag`);
274
+ tagsSelectStatements[idCount] = stmt;
275
+ return stmt;
276
+ };
277
+ const dedupeIds = (ids) => {
278
+ const seen = new Set();
279
+ const unique = [];
280
+ for (const id of ids) {
281
+ if (seen.has(id))
282
+ continue;
283
+ seen.add(id);
284
+ unique.push(id);
285
+ }
286
+ return unique;
287
+ };
288
+ const pushToMapArray = (map, key, value) => {
289
+ const existing = map.get(key);
290
+ if (existing) {
291
+ existing.push(value);
292
+ return;
293
+ }
294
+ map.set(key, [value]);
295
+ };
296
+ export const loadTagsForMemoryIds = (memoryIds) => {
297
+ const uniqueIds = dedupeIds(memoryIds);
298
+ if (uniqueIds.length === 0)
299
+ return new Map();
300
+ const stmt = getSelectTagsStatement(uniqueIds.length);
301
+ const rows = executeAll(stmt, ...uniqueIds);
302
+ const tagsById = new Map();
303
+ for (const row of rows) {
304
+ const memoryId = toSafeInteger(row.memory_id, 'memory_id');
305
+ const tag = toString(row.tag, 'tag');
306
+ pushToMapArray(tagsById, memoryId, tag);
307
+ }
308
+ return tagsById;
309
+ };
@@ -1,36 +1,48 @@
1
- import { db, executeGet, executeRun, mapRowToMemory, toSafeInteger, withImmediateTransaction, } from './db.js';
1
+ import { db, executeGet, executeRun, loadTagsForMemoryIds, mapRowToMemory, toSafeInteger, withImmediateTransaction, } from './db.js';
2
2
  const stmtGetMemoryByHash = db.prepare('SELECT * FROM memories WHERE hash = ?');
3
+ const stmtTouchMemoryByHash = db.prepare('UPDATE memories SET accessed_at = CURRENT_TIMESTAMP WHERE hash = ?');
3
4
  const stmtDeleteMemoryByHash = db.prepare('DELETE FROM memories WHERE hash = ?');
4
5
  export const getMemory = (hash) => {
5
- const row = executeGet(stmtGetMemoryByHash, hash);
6
- return row ? mapRowToMemory(row) : undefined;
6
+ return withImmediateTransaction(() => {
7
+ executeRun(stmtTouchMemoryByHash, hash);
8
+ const row = executeGet(stmtGetMemoryByHash, hash);
9
+ if (!row)
10
+ return undefined;
11
+ const id = toSafeInteger(row.id, 'id');
12
+ const tags = loadTagsForMemoryIds([id]).get(id) ?? [];
13
+ return mapRowToMemory(row, tags);
14
+ });
7
15
  };
8
16
  export const deleteMemory = (hash) => {
9
17
  const result = executeRun(stmtDeleteMemoryByHash, hash);
10
18
  return { changes: toSafeInteger(result.changes, 'changes') };
11
19
  };
20
+ const deleteMemoryForBatch = (hash) => {
21
+ try {
22
+ const result = deleteMemory(hash);
23
+ const deleted = result.changes > 0;
24
+ return deleted
25
+ ? { item: { hash, deleted: true }, succeeded: true }
26
+ : {
27
+ item: { hash, deleted: false, error: 'Memory not found' },
28
+ succeeded: false,
29
+ };
30
+ }
31
+ catch (err) {
32
+ const message = err instanceof Error ? err.message : 'Unknown error';
33
+ return { item: { hash, deleted: false, error: message }, succeeded: false };
34
+ }
35
+ };
12
36
  export const deleteMemories = (hashes) => {
13
37
  const results = [];
14
38
  let succeeded = 0;
15
39
  let failed = 0;
16
40
  return withImmediateTransaction(() => {
17
41
  for (const hash of hashes) {
18
- try {
19
- const result = deleteMemory(hash);
20
- if (result.changes > 0) {
21
- results.push({ hash, deleted: true });
22
- succeeded++;
23
- }
24
- else {
25
- results.push({ hash, deleted: false, error: 'Memory not found' });
26
- failed++;
27
- }
28
- }
29
- catch (err) {
30
- const message = err instanceof Error ? err.message : 'Unknown error';
31
- results.push({ hash, deleted: false, error: message });
32
- failed++;
33
- }
42
+ const outcome = deleteMemoryForBatch(hash);
43
+ results.push(outcome.item);
44
+ succeeded += outcome.succeeded ? 1 : 0;
45
+ failed += outcome.succeeded ? 0 : 1;
34
46
  }
35
47
  return { results, succeeded, failed };
36
48
  });
@@ -89,31 +89,55 @@ const createMemoryInTransaction = (input) => {
89
89
  return { id, hash, isNew };
90
90
  };
91
91
  export const createMemories = (items) => {
92
+ return withImmediateTransaction(() => createMemoriesInTransaction(items));
93
+ };
94
+ const withSavepoint = (name, fn) => {
95
+ db.exec(`SAVEPOINT ${name}`);
96
+ try {
97
+ const result = fn();
98
+ db.exec(`RELEASE ${name}`);
99
+ return result;
100
+ }
101
+ catch (err) {
102
+ db.exec(`ROLLBACK TO ${name}`);
103
+ db.exec(`RELEASE ${name}`);
104
+ throw err;
105
+ }
106
+ };
107
+ const createMemoriesInTransaction = (items) => {
92
108
  const results = [];
93
109
  let succeeded = 0;
94
110
  let failed = 0;
95
- return withImmediateTransaction(() => {
96
- for (let i = 0; i < items.length; i++) {
97
- const item = items[i];
98
- if (!item)
99
- continue;
100
- db.exec('SAVEPOINT mem_item');
101
- try {
102
- const { hash, isNew } = createMemoryInTransaction(item);
103
- results.push({ ok: true, index: i, hash, isNew });
104
- succeeded++;
105
- db.exec('RELEASE mem_item');
106
- }
107
- catch (err) {
108
- db.exec('ROLLBACK TO mem_item');
109
- db.exec('RELEASE mem_item');
110
- const message = err instanceof Error ? err.message : 'Unknown error';
111
- results.push({ ok: false, index: i, error: message });
112
- failed++;
113
- }
111
+ for (let i = 0; i < items.length; i++) {
112
+ const item = items[i];
113
+ if (!item)
114
+ continue;
115
+ const result = processCreateMemoriesItem(i, item);
116
+ results.push(result);
117
+ if (result.ok) {
118
+ succeeded++;
114
119
  }
115
- return { results, succeeded, failed };
116
- });
120
+ else {
121
+ failed++;
122
+ }
123
+ }
124
+ return { results, succeeded, failed };
125
+ };
126
+ const processCreateMemoriesItem = (index, item) => {
127
+ const savepointName = `mem_item_${index}`;
128
+ try {
129
+ const created = withSavepoint(savepointName, () => createMemoryInTransaction(item));
130
+ return {
131
+ ok: true,
132
+ index,
133
+ hash: created.hash,
134
+ isNew: created.isNew,
135
+ };
136
+ }
137
+ catch (err) {
138
+ const message = err instanceof Error ? err.message : 'Unknown error';
139
+ return { ok: false, index, error: message };
140
+ }
117
141
  };
118
142
  const stmtDeleteTagsForMemory = db.prepare('DELETE FROM tags WHERE memory_id = ?');
119
143
  const stmtUpdateContent = db.prepare('UPDATE memories SET content = ?, hash = ? WHERE id = ?');
@@ -121,6 +145,14 @@ const replaceTags = (memoryId, tags) => {
121
145
  executeRun(stmtDeleteTagsForMemory, memoryId);
122
146
  insertTags(memoryId, normalizeTags(tags, MAX_TAGS));
123
147
  };
148
+ const assertNoDuplicateOnUpdate = (oldHash, newHash) => {
149
+ if (newHash === oldHash)
150
+ return;
151
+ const existingId = findMemoryIdByHash(newHash);
152
+ if (existingId !== undefined) {
153
+ throw new Error('Content already exists as another memory');
154
+ }
155
+ };
124
156
  export const updateMemory = (hash, options) => {
125
157
  const memoryId = findMemoryIdByHash(hash);
126
158
  if (memoryId === undefined)
@@ -128,12 +160,7 @@ export const updateMemory = (hash, options) => {
128
160
  return withImmediateTransaction(() => {
129
161
  const newHash = buildHash(options.content);
130
162
  // Check if new content would create a duplicate
131
- if (newHash !== hash) {
132
- const existingId = findMemoryIdByHash(newHash);
133
- if (existingId !== undefined) {
134
- throw new Error('Content already exists as another memory');
135
- }
136
- }
163
+ assertNoDuplicateOnUpdate(hash, newHash);
137
164
  // Update content and hash
138
165
  executeRun(stmtUpdateContent, options.content, newHash, memoryId);
139
166
  // Update tags if provided, otherwise preserve existing tags
@@ -1,4 +1,4 @@
1
- import { db, executeAll, executeGet, executeRun, toSafeInteger, withImmediateTransaction, } from './db.js';
1
+ import { db, executeAll, executeGet, executeRun, mapRowToRelationship, toSafeInteger, withImmediateTransaction, } from './db.js';
2
2
  const stmtFindMemoryIdByHash = db.prepare('SELECT id FROM memories WHERE hash = ?');
3
3
  const findMemoryIdByHash = (hash) => {
4
4
  const row = executeGet(stmtFindMemoryIdByHash, hash);
@@ -13,18 +13,6 @@ const requireMemoryId = (hash) => {
13
13
  }
14
14
  return id;
15
15
  };
16
- const toString = (value, field) => {
17
- if (typeof value === 'string')
18
- return value;
19
- throw new Error(`Invalid ${field}`);
20
- };
21
- const mapRowToRelationship = (row) => ({
22
- id: toSafeInteger(row.id, 'id'),
23
- from_hash: toString(row.from_hash, 'from_hash'),
24
- to_hash: toString(row.to_hash, 'to_hash'),
25
- relation_type: toString(row.relation_type, 'relation_type'),
26
- created_at: toString(row.created_at, 'created_at'),
27
- });
28
16
  const stmtInsertRelationship = db.prepare(`
29
17
  INSERT OR IGNORE INTO relationships (from_memory_id, to_memory_id, relation_type)
30
18
  VALUES (?, ?, ?)
@@ -60,13 +48,14 @@ const buildGetRelationshipsQuery = (direction) => {
60
48
  JOIN memories mf ON r.from_memory_id = mf.id
61
49
  JOIN memories mt ON r.to_memory_id = mt.id
62
50
  `;
51
+ const orderBy = ' ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id';
63
52
  switch (direction) {
64
53
  case 'outgoing':
65
- return `${baseSelect} WHERE mf.hash = ?`;
54
+ return `${baseSelect} WHERE mf.hash = ?${orderBy}`;
66
55
  case 'incoming':
67
- return `${baseSelect} WHERE mt.hash = ?`;
56
+ return `${baseSelect} WHERE mt.hash = ?${orderBy}`;
68
57
  case 'both':
69
- return `${baseSelect} WHERE mf.hash = ? OR mt.hash = ?`;
58
+ return `${baseSelect} WHERE mf.hash = ? OR mt.hash = ?${orderBy}`;
70
59
  }
71
60
  };
72
61
  const stmtGetRelationships = {
@@ -1,4 +1,4 @@
1
- import { db, executeAll, mapRowToSearchResult, prepareCached, toSafeInteger, } from './db.js';
1
+ import { db, executeAll, loadTagsForMemoryIds, mapRowToRelationship, mapRowToSearchResult, prepareCached, toSafeInteger, } from './db.js';
2
2
  const MAX_QUERY_TOKENS = 50;
3
3
  const DEFAULT_LIMIT = 100;
4
4
  const RECENCY_DECAY_DAYS = 7;
@@ -65,25 +65,16 @@ const QUERY_INVALID_TOKENS = ['fts5', 'syntax error'];
65
65
  const isSearchIndexMissing = (message) => INDEX_MISSING_TOKENS.some((token) => message.includes(token));
66
66
  const isSearchQueryInvalid = (message) => QUERY_INVALID_TOKENS.some((token) => message.includes(token));
67
67
  const getErrorMessage = (err) => err instanceof Error ? err.message : String(err);
68
- const SEARCH_ERROR_MAP = [
69
- {
70
- matches: isSearchIndexMissing,
71
- build: () => new Error('Search index unavailable. Ensure FTS5 is enabled and the index is ' +
72
- 'initialized.'),
73
- },
74
- {
75
- matches: isSearchQueryInvalid,
76
- build: (message) => new Error('Invalid search query syntax. Check for unbalanced quotes or special ' +
77
- 'characters. ' +
78
- `Details: ${message}`),
79
- },
80
- ];
81
68
  const toSearchError = (err) => {
82
69
  const message = getErrorMessage(err);
83
- for (const mapping of SEARCH_ERROR_MAP) {
84
- if (mapping.matches(message)) {
85
- return mapping.build(message);
86
- }
70
+ if (isSearchIndexMissing(message)) {
71
+ return new Error('Search index unavailable. Ensure FTS5 is enabled and the index is ' +
72
+ 'initialized.');
73
+ }
74
+ if (isSearchQueryInvalid(message)) {
75
+ return new Error('Invalid search query syntax. Check for unbalanced quotes or special ' +
76
+ 'characters. ' +
77
+ `Details: ${message}`);
87
78
  }
88
79
  return undefined;
89
80
  };
@@ -93,13 +84,17 @@ const executeSearch = (sql, params) => {
93
84
  return executeAll(stmt, ...params);
94
85
  }
95
86
  catch (err) {
96
- const mappedError = toSearchError(err);
97
- if (mappedError) {
98
- throw mappedError;
99
- }
100
- throw err;
87
+ throw toSearchError(err) ?? err;
101
88
  }
102
89
  };
90
+ const mapRowsToSearchResultsWithTags = (rows) => {
91
+ const ids = rows.map((row) => toSafeInteger(row.id, 'id'));
92
+ const tagsById = loadTagsForMemoryIds(ids);
93
+ return rows.map((row) => {
94
+ const id = toSafeInteger(row.id, 'id');
95
+ return mapRowToSearchResult(row, tagsById.get(id) ?? []);
96
+ });
97
+ };
103
98
  export const searchMemories = (input) => {
104
99
  const tokens = tokenizeQuery(input.query);
105
100
  if (tokens.length === 0) {
@@ -107,22 +102,10 @@ export const searchMemories = (input) => {
107
102
  }
108
103
  const { sql, params } = buildSearchQuery(tokens);
109
104
  const rows = executeSearch(sql, params);
110
- return rows.map((row) => mapRowToSearchResult(row));
105
+ return mapRowsToSearchResultsWithTags(rows);
111
106
  };
112
107
  const MAX_RECALL_DEPTH = 3;
113
108
  const MAX_RECALL_MEMORIES = 50;
114
- const toString = (value, field) => {
115
- if (typeof value === 'string')
116
- return value;
117
- throw new Error(`Invalid ${field}`);
118
- };
119
- const mapRowToRelationship = (row) => ({
120
- id: toSafeInteger(row.id, 'id'),
121
- from_hash: toString(row.from_hash, 'from_hash'),
122
- to_hash: toString(row.to_hash, 'to_hash'),
123
- relation_type: toString(row.relation_type, 'relation_type'),
124
- created_at: toString(row.created_at, 'created_at'),
125
- });
126
109
  const buildRecallQuery = (seedCount, depth) => {
127
110
  const seedPlaceholders = Array.from({ length: seedCount }, () => '?').join(', ');
128
111
  const safeDepth = Math.min(Math.max(0, depth), MAX_RECALL_DEPTH);
@@ -166,30 +149,53 @@ const buildRelationshipsQuery = (memoryCount) => {
166
149
  JOIN memories mt ON r.to_memory_id = mt.id
167
150
  WHERE r.from_memory_id IN (${placeholders})
168
151
  AND r.to_memory_id IN (${placeholders})
152
+ ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
169
153
  `;
170
154
  return { sql };
171
155
  };
156
+ const executeWithSql = (sql, params) => {
157
+ const stmt = db.prepare(sql);
158
+ return executeAll(stmt, ...params);
159
+ };
160
+ const executeRecall = (seedIds, depth) => {
161
+ const { sql } = buildRecallQuery(seedIds.length, depth);
162
+ return executeWithSql(sql, seedIds);
163
+ };
164
+ const loadRelationshipsForMemoryIds = (memoryIds) => {
165
+ const { sql } = buildRelationshipsQuery(memoryIds.length);
166
+ const rows = executeWithSql(sql, [...memoryIds, ...memoryIds]);
167
+ return rows.map(mapRowToRelationship);
168
+ };
169
+ const getRecallDepth = (depth) => depth ?? 1;
170
+ const emptyRecallResult = (depth) => ({
171
+ memories: [],
172
+ relationships: [],
173
+ depth,
174
+ });
175
+ const recallAtDepthZero = (searchResults, depth) => {
176
+ if (depth !== 0)
177
+ return undefined;
178
+ return { memories: searchResults, relationships: [], depth };
179
+ };
180
+ const recallAtPositiveDepth = (searchResults, depth) => {
181
+ const seedIds = searchResults.map((m) => m.id);
182
+ const recallRows = executeRecall(seedIds, depth);
183
+ const memories = mapRowsToSearchResultsWithTags(recallRows);
184
+ if (memories.length === 0) {
185
+ return emptyRecallResult(depth);
186
+ }
187
+ const relationships = loadRelationshipsForMemoryIds(memories.map((m) => m.id));
188
+ return { memories, relationships, depth };
189
+ };
172
190
  export const recallMemories = (input) => {
173
- const depth = input.depth ?? 1;
174
191
  const searchResults = searchMemories({ query: input.query });
175
192
  if (searchResults.length === 0) {
176
- return { memories: [], relationships: [], depth };
177
- }
178
- if (depth === 0) {
179
- return { memories: searchResults, relationships: [], depth };
193
+ return emptyRecallResult(getRecallDepth(input.depth));
180
194
  }
181
- const seedIds = searchResults.map((m) => m.id);
182
- const { sql: recallSql } = buildRecallQuery(seedIds.length, depth);
183
- const recallStmt = db.prepare(recallSql);
184
- const recallRows = executeAll(recallStmt, ...seedIds);
185
- const memories = recallRows.map(mapRowToSearchResult);
186
- const allMemoryIds = memories.map((m) => m.id);
187
- if (allMemoryIds.length === 0) {
188
- return { memories: [], relationships: [], depth };
195
+ const depth = getRecallDepth(input.depth);
196
+ const depthZeroResult = recallAtDepthZero(searchResults, depth);
197
+ if (depthZeroResult) {
198
+ return depthZeroResult;
189
199
  }
190
- const { sql: relSql } = buildRelationshipsQuery(allMemoryIds.length);
191
- const relStmt = db.prepare(relSql);
192
- const relRows = executeAll(relStmt, ...allMemoryIds, ...allMemoryIds);
193
- const relationships = relRows.map(mapRowToRelationship);
194
- return { memories, relationships, depth };
200
+ return recallAtPositiveDepth(searchResults, depth);
195
201
  };
package/dist/index.js CHANGED
@@ -19,22 +19,38 @@ const readPackageVersion = async () => {
19
19
  const version = Reflect.get(parsed, 'version');
20
20
  return typeof version === 'string' ? version : undefined;
21
21
  };
22
- const readServerInstructions = async () => {
22
+ const toNonEmptyTrimmedOrUndefined = (text) => {
23
+ const trimmed = text.trim();
24
+ return trimmed.length > 0 ? trimmed : undefined;
25
+ };
26
+ const readTextFileOrUndefined = async (url) => {
23
27
  try {
24
- const text = await readFile(new URL('./instructions.md', import.meta.url), {
28
+ return await readFile(url, {
25
29
  encoding: 'utf-8',
26
30
  signal: AbortSignal.timeout(5000),
27
31
  });
28
- const trimmed = text.trim();
29
- return trimmed.length > 0 ? trimmed : undefined;
30
32
  }
31
33
  catch {
32
34
  return undefined;
33
35
  }
34
36
  };
35
- const packageVersion = await readPackageVersion();
36
- const serverInstructions = (await readServerInstructions()) ??
37
- 'A Memory MCP Server for AI Assistants using node:sqlite';
37
+ const readServerInstructions = async () => {
38
+ const text = await readTextFileOrUndefined(new URL('./instructions.md', import.meta.url));
39
+ if (!text)
40
+ return undefined;
41
+ return toNonEmptyTrimmedOrUndefined(text);
42
+ };
43
+ const loadServerMetadata = async () => {
44
+ const [packageVersion, instructions] = await Promise.all([
45
+ readPackageVersion(),
46
+ readServerInstructions(),
47
+ ]);
48
+ return {
49
+ packageVersion,
50
+ instructions: instructions ?? 'A Memory MCP Server for AI Assistants using node:sqlite',
51
+ };
52
+ };
53
+ const { packageVersion, instructions: serverInstructions } = await loadServerMetadata();
38
54
  const server = new McpServer({ name: 'memdb', version: packageVersion ?? '0.0.0' }, {
39
55
  instructions: serverInstructions,
40
56
  capabilities: { tools: {} },
@@ -42,7 +58,7 @@ const server = new McpServer({ name: 'memdb', version: packageVersion ?? '0.0.0'
42
58
  const patchToolErrorResults = (target) => {
43
59
  const targetUnknown = target;
44
60
  const existing = Reflect.get(targetUnknown, 'createToolError');
45
- if (existing !== undefined && typeof existing !== 'function')
61
+ if (typeof existing !== 'function')
46
62
  return;
47
63
  const createToolError = (message) => {
48
64
  const structured = {
@@ -62,34 +78,57 @@ registerAllTools(server);
62
78
  let transport;
63
79
  let shuttingDown = false;
64
80
  const SHUTDOWN_TIMEOUT = 5000;
65
- async function shutdown(signal) {
66
- if (shuttingDown)
67
- return;
68
- shuttingDown = true;
69
- logger.info(`Received ${signal}, shutting down gracefully...`);
70
- const forceExitTimer = setTimeout(() => {
81
+ const createShutdownTimer = () => {
82
+ return setTimeout(() => {
71
83
  logger.warn('Shutdown timeout exceeded, forcing exit');
72
84
  process.exit(1);
73
85
  }, SHUTDOWN_TIMEOUT);
74
- try {
75
- closeDb();
76
- await transport?.close();
77
- clearTimeout(forceExitTimer);
78
- process.exit(0);
79
- }
80
- catch (err) {
81
- logger.error('Error during shutdown:', err);
82
- clearTimeout(forceExitTimer);
83
- process.exit(1);
86
+ };
87
+ const closeServerResources = async () => {
88
+ closeDb();
89
+ await transport?.close();
90
+ };
91
+ const clearTimerAndExit = (timer, code) => {
92
+ clearTimeout(timer);
93
+ process.exit(code);
94
+ };
95
+ const exitWithShutdownTimer = (timer, code, error) => {
96
+ if (error !== undefined) {
97
+ logger.error('Error during shutdown:', error);
84
98
  }
99
+ clearTimerAndExit(timer, code);
100
+ };
101
+ const beginShutdown = (signal) => {
102
+ logger.info(`Received ${signal}, shutting down gracefully...`);
103
+ const forceExitTimer = createShutdownTimer();
104
+ void closeServerResources()
105
+ .then(() => {
106
+ exitWithShutdownTimer(forceExitTimer, 0);
107
+ })
108
+ .catch((err) => {
109
+ exitWithShutdownTimer(forceExitTimer, 1, err);
110
+ });
111
+ };
112
+ function shutdown(signal) {
113
+ if (shuttingDown)
114
+ return;
115
+ shuttingDown = true;
116
+ beginShutdown(signal);
85
117
  }
118
+ const createTransport = () => {
119
+ const stdioTransport = new BatchRejectingStdioServerTransport();
120
+ const supportedProtocolVersions = SUPPORTED_PROTOCOL_VERSIONS.filter((version) => version !== '2025-03-26');
121
+ return new ProtocolVersionGuardTransport(stdioTransport, supportedProtocolVersions);
122
+ };
123
+ const connectServer = async (transportToUse) => {
124
+ transport = transportToUse;
125
+ await server.connect(transportToUse);
126
+ logger.info('Memory MCP Server running on stdio');
127
+ };
86
128
  const main = async () => {
87
129
  try {
88
- const stdioTransport = new BatchRejectingStdioServerTransport();
89
- const guardedTransport = new ProtocolVersionGuardTransport(stdioTransport, SUPPORTED_PROTOCOL_VERSIONS);
90
- transport = guardedTransport;
91
- await server.connect(guardedTransport);
92
- logger.info('Memory MCP Server running on stdio');
130
+ const guardedTransport = createTransport();
131
+ await connectServer(guardedTransport);
93
132
  }
94
133
  catch (error) {
95
134
  logger.error('Failed to start server', error);
@@ -99,7 +138,9 @@ const main = async () => {
99
138
  const registerSignalHandlers = () => {
100
139
  const signals = ['SIGTERM', 'SIGINT', 'SIGBREAK'];
101
140
  for (const signal of signals) {
102
- process.on(signal, () => void shutdown(signal));
141
+ process.on(signal, () => {
142
+ shutdown(signal);
143
+ });
103
144
  }
104
145
  };
105
146
  const registerProcessHandlers = () => {
@@ -24,6 +24,7 @@ Use this server to store and retrieve persistent memories (facts, decisions, les
24
24
  - `tags` (1–100 items, no whitespace, `kebab-case`)
25
25
  - `importance` (0–10, 10=critical)
26
26
  - `memory_type` (`general`, `fact`, `plan`, `decision`, `reflection`, `lesson`, `error`, `gradient`)
27
+ - `accessed_at` is updated when you call `get_memory`
27
28
  - **Relationship:** Directed edge (`from_hash` → `to_hash`) with a typed label (`relation_type`).
28
29
 
29
30
  ## Workflows
@@ -71,6 +72,7 @@ Full-text + tag search.
71
72
  - **Use when:** Discovering what exists; start here before mutating.
72
73
  - **Args:** `query` (1–1,000 chars).
73
74
  - **Returns:** Array of `Memory` + `relevance`, up to 100 results.
75
+ - **Returns:** Array of `Memory` + `relevance`, up to 100 results (includes `tags`).
74
76
  - **Notes:** Content matches rank higher than tag matches.
75
77
 
76
78
  ### get_memory
@@ -79,7 +81,8 @@ Fetch a single memory by hash.
79
81
 
80
82
  - **Use when:** You need verbatim content after search identified a hash.
81
83
  - **Args:** `hash` (32 hex chars).
82
- - **Returns:** `Memory` object.
84
+ - **Returns:** `Memory` object (includes `tags`).
85
+ - **Notes:** Updates `accessed_at` on read.
83
86
 
84
87
  ### update_memory
85
88
 
@@ -136,7 +139,7 @@ Search + traverse relationships to return a connected cluster.
136
139
 
137
140
  - **Use when:** You need broader context beyond keyword matches.
138
141
  - **Args:** `query`, `depth` (opt, 0–3; default 1).
139
- - **Returns:** `{ memories, relationships, depth }`.
142
+ - **Returns:** `{ memories, relationships, depth }` where `memories` include `tags`.
140
143
 
141
144
  ### memory_stats
142
145
 
@@ -14,4 +14,7 @@ export declare class ProtocolVersionGuardTransport implements Transport {
14
14
  send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void>;
15
15
  close(): Promise<void>;
16
16
  private handleMessage;
17
+ private handleInitialize;
18
+ private handleInitializedNotification;
19
+ private handleBeforeReady;
17
20
  }
@@ -82,35 +82,46 @@ export class ProtocolVersionGuardTransport {
82
82
  }
83
83
  const initializeInfo = getInitializeInfo(message);
84
84
  if (initializeInfo) {
85
- if (this.sawInitialize) {
86
- void this.inner.send(createLifecycleError(initializeInfo.id, 'Invalid request: initialize already received'), { relatedRequestId: initializeInfo.id });
87
- return;
88
- }
89
- if (!this.supportedVersions.includes(initializeInfo.protocolVersion)) {
90
- void this.inner.send(createUnsupportedVersionError(initializeInfo.id, initializeInfo.protocolVersion, this.supportedVersions), { relatedRequestId: initializeInfo.id });
91
- return;
92
- }
93
- this.sawInitialize = true;
94
- this.onmessage(message, extra);
85
+ this.handleInitialize(message, initializeInfo, extra);
95
86
  return;
96
87
  }
97
88
  if (isInitializedNotification(message)) {
98
- if (this.sawInitialize && !this.ready) {
99
- this.ready = true;
100
- this.onmessage(message, extra);
101
- }
89
+ this.handleInitializedNotification(message, extra);
102
90
  return;
103
91
  }
104
92
  if (!this.sawInitialize || !this.ready) {
105
- if (isJSONRPCRequest(message)) {
106
- void this.inner.send(createLifecycleError(message.id, 'Invalid request: initialize must be sent before other requests'), { relatedRequestId: message.id });
107
- }
108
- else if (isJSONRPCResultResponse(message) ||
109
- isJSONRPCErrorResponse(message)) {
110
- this.onmessage(message, extra);
111
- }
93
+ this.handleBeforeReady(message, extra);
94
+ return;
95
+ }
96
+ this.onmessage(message, extra);
97
+ }
98
+ handleInitialize(message, initializeInfo, extra) {
99
+ if (this.sawInitialize) {
100
+ void this.inner.send(createLifecycleError(initializeInfo.id, 'Invalid request: initialize already received'), { relatedRequestId: initializeInfo.id });
101
+ return;
102
+ }
103
+ if (!this.supportedVersions.includes(initializeInfo.protocolVersion)) {
104
+ void this.inner.send(createUnsupportedVersionError(initializeInfo.id, initializeInfo.protocolVersion, this.supportedVersions), { relatedRequestId: initializeInfo.id });
112
105
  return;
113
106
  }
107
+ this.sawInitialize = true;
108
+ this.onmessage(message, extra);
109
+ }
110
+ handleInitializedNotification(message, extra) {
111
+ if (!this.sawInitialize)
112
+ return;
113
+ if (this.ready)
114
+ return;
115
+ this.ready = true;
114
116
  this.onmessage(message, extra);
115
117
  }
118
+ handleBeforeReady(message, extra) {
119
+ if (isJSONRPCRequest(message)) {
120
+ void this.inner.send(createLifecycleError(message.id, 'Invalid request: initialize must be sent before other requests'), { relatedRequestId: message.id });
121
+ return;
122
+ }
123
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
124
+ this.onmessage(message, extra);
125
+ }
126
+ }
116
127
  }
@@ -25,5 +25,8 @@ export declare class BatchRejectingStdioServerTransport implements Transport {
25
25
  private handleBatch;
26
26
  private handleNonBatch;
27
27
  private handleLine;
28
+ private handleLineSafely;
29
+ private drainReadBuffer;
30
+ private handleReadBufferError;
28
31
  private processReadBuffer;
29
32
  }
@@ -47,21 +47,32 @@ class LineBuffer {
47
47
  }
48
48
  buildLineBuffer(chunkIndex, newlineIndex, lineLength) {
49
49
  const lineBuffer = Buffer.allocUnsafe(lineLength);
50
+ const writeOffset = this.copyChunksBeforeIndex(lineBuffer, chunkIndex);
51
+ this.copyChunkPrefix(lineBuffer, chunkIndex, newlineIndex, writeOffset);
52
+ return lineBuffer;
53
+ }
54
+ copyChunksBeforeIndex(target, chunkIndex) {
50
55
  let writeOffset = 0;
51
56
  for (let i = 0; i < chunkIndex; i++) {
52
57
  const part = this.chunks[i];
53
58
  if (!part)
54
59
  continue;
55
- part.copy(lineBuffer, writeOffset);
60
+ part.copy(target, writeOffset);
56
61
  writeOffset += part.length;
57
62
  }
63
+ return writeOffset;
64
+ }
65
+ copyChunkPrefix(target, chunkIndex, newlineIndex, writeOffset) {
58
66
  const chunk = this.chunks[chunkIndex];
59
- if (chunk && newlineIndex > 0) {
60
- chunk.copy(lineBuffer, writeOffset, 0, newlineIndex);
61
- }
62
- return lineBuffer;
67
+ if (!chunk || newlineIndex <= 0)
68
+ return;
69
+ chunk.copy(target, writeOffset, 0, newlineIndex);
63
70
  }
64
71
  consumeLine(chunkIndex, newlineIndex, lineLength) {
72
+ this.chunks = this.buildRemainingChunks(chunkIndex, newlineIndex);
73
+ this.totalLength -= lineLength + 1;
74
+ }
75
+ buildRemainingChunks(chunkIndex, newlineIndex) {
65
76
  const remaining = [];
66
77
  const chunk = this.chunks[chunkIndex];
67
78
  if (chunk) {
@@ -75,8 +86,7 @@ class LineBuffer {
75
86
  continue;
76
87
  remaining.push(tail);
77
88
  }
78
- this.chunks = remaining;
79
- this.totalLength -= lineLength + 1;
89
+ return remaining;
80
90
  }
81
91
  clear() {
82
92
  this.chunks = [];
@@ -95,24 +105,17 @@ const getRequestIdResult = (value) => {
95
105
  }
96
106
  return { ok: false, reason: 'invalid-type' };
97
107
  };
98
- const invalidRequestError = (id) => id === undefined
99
- ? {
100
- jsonrpc: '2.0',
101
- error: {
102
- code: -32600,
103
- message: 'Invalid request',
104
- },
105
- }
106
- : {
107
- jsonrpc: '2.0',
108
- id,
109
- error: {
110
- code: -32600,
111
- message: 'Invalid request',
112
- },
113
- };
108
+ const invalidRequestError = (id) => ({
109
+ jsonrpc: '2.0',
110
+ id,
111
+ error: {
112
+ code: -32600,
113
+ message: 'Invalid request',
114
+ },
115
+ });
114
116
  const parseError = () => ({
115
117
  jsonrpc: '2.0',
118
+ id: null,
116
119
  error: {
117
120
  code: -32700,
118
121
  message: 'Parse error',
@@ -183,7 +186,7 @@ export class BatchRejectingStdioServerTransport {
183
186
  void this.send(invalidRequestError(id));
184
187
  }
185
188
  sendInvalidRequestUnknownId() {
186
- void this.send(invalidRequestError());
189
+ void this.send(invalidRequestError(null));
187
190
  }
188
191
  sendParseError() {
189
192
  void this.send(parseError());
@@ -231,20 +234,29 @@ export class BatchRejectingStdioServerTransport {
231
234
  }
232
235
  this.onmessage(parsed.data);
233
236
  }
237
+ handleLineSafely(line) {
238
+ try {
239
+ this.handleLine(line);
240
+ }
241
+ catch (error) {
242
+ this.onerror(error instanceof Error ? error : new Error(String(error)));
243
+ }
244
+ }
245
+ drainReadBuffer() {
246
+ for (let line = this.readBuffer.readLine(); line !== null; line = this.readBuffer.readLine()) {
247
+ this.handleLineSafely(line);
248
+ }
249
+ }
250
+ handleReadBufferError() {
251
+ this.readBuffer.clear();
252
+ this.sendParseError();
253
+ }
234
254
  processReadBuffer() {
235
255
  try {
236
- for (let line = this.readBuffer.readLine(); line !== null; line = this.readBuffer.readLine()) {
237
- try {
238
- this.handleLine(line);
239
- }
240
- catch (error) {
241
- this.onerror(error instanceof Error ? error : new Error(String(error)));
242
- }
243
- }
256
+ this.drainReadBuffer();
244
257
  }
245
258
  catch {
246
- this.readBuffer.clear();
247
- this.sendParseError();
259
+ this.handleReadBufferError();
248
260
  }
249
261
  }
250
262
  }
package/dist/tools.js CHANGED
@@ -17,10 +17,11 @@ const defaultDeps = {
17
17
  deleteRelationship,
18
18
  recallMemories,
19
19
  };
20
+ const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0;
20
21
  const getErrorMessage = (error) => {
21
22
  if (error instanceof Error)
22
23
  return error.message;
23
- if (typeof error === 'string' && error.length > 0)
24
+ if (isNonEmptyString(error))
24
25
  return error;
25
26
  return 'Unknown error';
26
27
  };
@@ -43,132 +44,142 @@ const ok = (result) => {
43
44
  structuredContent: structured,
44
45
  };
45
46
  };
47
+ const runHandlerSafely = async (code, handler, params) => {
48
+ try {
49
+ return await handler(params);
50
+ }
51
+ catch (err) {
52
+ return createErrorResponse(code, getErrorMessage(err));
53
+ }
54
+ };
46
55
  const wrapHandler = (code, handler) => {
47
- return async (params) => {
48
- try {
49
- return await handler(params);
50
- }
51
- catch (err) {
52
- return createErrorResponse(code, getErrorMessage(err));
53
- }
54
- };
56
+ return async (params) => runHandlerSafely(code, handler, params);
55
57
  };
56
58
  const normalizeHash = (hash) => hash.toLowerCase();
57
- const buildCoreTools = (deps) => [
58
- {
59
- name: 'store_memory',
60
- options: {
61
- title: 'Store Memory',
62
- description: 'Store a new memory with tags',
63
- inputSchema: StoreMemoryInputSchema,
64
- outputSchema: DefaultOutputSchema,
65
- annotations: { idempotentHint: true },
66
- },
67
- handler: wrapHandler('E_STORE_MEMORY', async (params) => {
68
- const input = StoreMemoryInputSchema.parse(params);
69
- const result = await deps.createMemory({
70
- content: input.content,
71
- tags: input.tags,
72
- ...(input.importance !== undefined && { importance: input.importance }),
73
- ...(input.memory_type !== undefined && {
74
- memory_type: input.memory_type,
75
- }),
76
- });
77
- return ok(result);
78
- }),
59
+ const toCreateMemoryInput = (input) => ({
60
+ content: input.content,
61
+ tags: input.tags,
62
+ ...(input.importance === undefined ? {} : { importance: input.importance }),
63
+ ...(input.memory_type === undefined
64
+ ? {}
65
+ : { memory_type: input.memory_type }),
66
+ });
67
+ const buildStoreMemoryTool = (deps) => ({
68
+ name: 'store_memory',
69
+ options: {
70
+ title: 'Store Memory',
71
+ description: 'Store a new memory with tags',
72
+ inputSchema: StoreMemoryInputSchema,
73
+ outputSchema: DefaultOutputSchema,
74
+ annotations: { idempotentHint: true },
79
75
  },
80
- {
81
- name: 'store_memories',
82
- options: {
83
- title: 'Store Multiple Memories',
84
- description: 'Store multiple memories in a single batch operation. Returns per-item results with partial success support.',
85
- inputSchema: StoreMemoriesInputSchema,
86
- outputSchema: DefaultOutputSchema,
87
- annotations: { idempotentHint: true },
88
- },
89
- handler: wrapHandler('E_STORE_MEMORIES', async (params) => {
90
- const input = StoreMemoriesInputSchema.parse(params);
91
- const items = input.items.map((item) => ({
92
- content: item.content,
93
- tags: item.tags,
94
- ...(item.importance !== undefined && { importance: item.importance }),
95
- ...(item.memory_type !== undefined && {
96
- memory_type: item.memory_type,
97
- }),
98
- }));
99
- const result = await deps.createMemories(items);
100
- return ok(result);
101
- }),
76
+ handler: wrapHandler('E_STORE_MEMORY', async (params) => {
77
+ const input = StoreMemoryInputSchema.parse(params);
78
+ const result = await deps.createMemory(toCreateMemoryInput({
79
+ content: input.content,
80
+ tags: input.tags,
81
+ importance: input.importance,
82
+ memory_type: input.memory_type,
83
+ }));
84
+ return ok(result);
85
+ }),
86
+ });
87
+ const buildStoreMemoriesTool = (deps) => ({
88
+ name: 'store_memories',
89
+ options: {
90
+ title: 'Store Multiple Memories',
91
+ description: 'Store multiple memories in a single batch operation. Returns per-item results with partial success support.',
92
+ inputSchema: StoreMemoriesInputSchema,
93
+ outputSchema: DefaultOutputSchema,
94
+ annotations: { idempotentHint: true },
102
95
  },
103
- {
104
- name: 'get_memory',
105
- options: {
106
- title: 'Get Memory',
107
- description: 'Retrieve memory by hash',
108
- inputSchema: GetMemoryInputSchema,
109
- outputSchema: DefaultOutputSchema,
110
- annotations: { readOnlyHint: true },
111
- },
112
- handler: wrapHandler('E_GET_MEMORY', async (params) => {
113
- const input = GetMemoryInputSchema.parse(params);
114
- const result = await deps.getMemory(normalizeHash(input.hash));
115
- if (!result) {
116
- return createErrorResponse('E_NOT_FOUND', 'Memory not found');
117
- }
118
- return ok(result);
119
- }),
96
+ handler: wrapHandler('E_STORE_MEMORIES', async (params) => {
97
+ const input = StoreMemoriesInputSchema.parse(params);
98
+ const items = input.items.map((item) => toCreateMemoryInput({
99
+ content: item.content,
100
+ tags: item.tags,
101
+ importance: item.importance,
102
+ memory_type: item.memory_type,
103
+ }));
104
+ const result = await deps.createMemories(items);
105
+ return ok(result);
106
+ }),
107
+ });
108
+ const buildGetMemoryTool = (deps) => ({
109
+ name: 'get_memory',
110
+ options: {
111
+ title: 'Get Memory',
112
+ description: 'Retrieve memory by hash',
113
+ inputSchema: GetMemoryInputSchema,
114
+ outputSchema: DefaultOutputSchema,
120
115
  },
121
- {
122
- name: 'delete_memory',
123
- options: {
124
- title: 'Delete Memory',
125
- description: 'Delete by hash',
126
- inputSchema: DeleteMemoryInputSchema,
127
- outputSchema: DefaultOutputSchema,
128
- annotations: { destructiveHint: true },
129
- },
130
- handler: wrapHandler('E_DELETE_MEMORY', async (params) => {
131
- const input = DeleteMemoryInputSchema.parse(params);
132
- const result = await deps.deleteMemory(normalizeHash(input.hash));
133
- if (result.changes === 0) {
134
- return createErrorResponse('E_NOT_FOUND', 'Memory not found');
135
- }
136
- return ok({ deleted: true });
137
- }),
116
+ handler: wrapHandler('E_GET_MEMORY', async (params) => {
117
+ const input = GetMemoryInputSchema.parse(params);
118
+ const result = await deps.getMemory(normalizeHash(input.hash));
119
+ if (!result) {
120
+ return createErrorResponse('E_NOT_FOUND', 'Memory not found');
121
+ }
122
+ return ok(result);
123
+ }),
124
+ });
125
+ const buildDeleteMemoryTool = (deps) => ({
126
+ name: 'delete_memory',
127
+ options: {
128
+ title: 'Delete Memory',
129
+ description: 'Delete by hash',
130
+ inputSchema: DeleteMemoryInputSchema,
131
+ outputSchema: DefaultOutputSchema,
132
+ annotations: { destructiveHint: true },
138
133
  },
139
- {
140
- name: 'delete_memories',
141
- options: {
142
- title: 'Delete Multiple Memories',
143
- description: 'Delete multiple memories by hash in a single batch operation. Returns per-item results with partial success support.',
144
- inputSchema: DeleteMemoriesInputSchema,
145
- outputSchema: DefaultOutputSchema,
146
- annotations: { destructiveHint: true },
147
- },
148
- handler: wrapHandler('E_DELETE_MEMORIES', async (params) => {
149
- const input = DeleteMemoriesInputSchema.parse(params);
150
- const result = await deps.deleteMemories(input.hashes.map(normalizeHash));
151
- return ok(result);
152
- }),
134
+ handler: wrapHandler('E_DELETE_MEMORY', async (params) => {
135
+ const input = DeleteMemoryInputSchema.parse(params);
136
+ const result = await deps.deleteMemory(normalizeHash(input.hash));
137
+ if (result.changes === 0) {
138
+ return createErrorResponse('E_NOT_FOUND', 'Memory not found');
139
+ }
140
+ return ok({ deleted: true });
141
+ }),
142
+ });
143
+ const buildDeleteMemoriesTool = (deps) => ({
144
+ name: 'delete_memories',
145
+ options: {
146
+ title: 'Delete Multiple Memories',
147
+ description: 'Delete multiple memories by hash in a single batch operation. Returns per-item results with partial success support.',
148
+ inputSchema: DeleteMemoriesInputSchema,
149
+ outputSchema: DefaultOutputSchema,
150
+ annotations: { destructiveHint: true },
153
151
  },
154
- {
155
- name: 'update_memory',
156
- options: {
157
- title: 'Update Memory',
158
- description: 'Update memory content. Returns new hash since content change affects the hash.',
159
- inputSchema: UpdateMemoryInputSchema,
160
- outputSchema: DefaultOutputSchema,
161
- annotations: { idempotentHint: true },
162
- },
163
- handler: wrapHandler('E_UPDATE_MEMORY', async (params) => {
164
- const input = UpdateMemoryInputSchema.parse(params);
165
- const result = await deps.updateMemory(normalizeHash(input.hash), {
166
- content: input.content,
167
- tags: input.tags,
168
- });
169
- return ok(result);
170
- }),
152
+ handler: wrapHandler('E_DELETE_MEMORIES', async (params) => {
153
+ const input = DeleteMemoriesInputSchema.parse(params);
154
+ const result = await deps.deleteMemories(input.hashes.map(normalizeHash));
155
+ return ok(result);
156
+ }),
157
+ });
158
+ const buildUpdateMemoryTool = (deps) => ({
159
+ name: 'update_memory',
160
+ options: {
161
+ title: 'Update Memory',
162
+ description: 'Update memory content. Returns new hash since content change affects the hash.',
163
+ inputSchema: UpdateMemoryInputSchema,
164
+ outputSchema: DefaultOutputSchema,
165
+ annotations: { idempotentHint: true },
171
166
  },
167
+ handler: wrapHandler('E_UPDATE_MEMORY', async (params) => {
168
+ const input = UpdateMemoryInputSchema.parse(params);
169
+ const result = await deps.updateMemory(normalizeHash(input.hash), {
170
+ content: input.content,
171
+ tags: input.tags,
172
+ });
173
+ return ok(result);
174
+ }),
175
+ });
176
+ const buildCoreTools = (deps) => [
177
+ buildStoreMemoryTool(deps),
178
+ buildStoreMemoriesTool(deps),
179
+ buildGetMemoryTool(deps),
180
+ buildDeleteMemoryTool(deps),
181
+ buildDeleteMemoriesTool(deps),
182
+ buildUpdateMemoryTool(deps),
172
183
  ];
173
184
  const buildSearchTools = (deps) => [
174
185
  {
package/dist/types.d.ts CHANGED
@@ -4,6 +4,7 @@ export interface Memory {
4
4
  readonly id: number;
5
5
  readonly content: string;
6
6
  readonly summary: string | undefined;
7
+ readonly tags: string[];
7
8
  readonly importance: number;
8
9
  readonly memory_type: MemoryType;
9
10
  readonly created_at: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/memdb",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "mcpName": "io.github.j0hanz/memdb",
5
5
  "description": "A SQLite-backed MCP memory server with local workspace storage.",
6
6
  "type": "module",