@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 +7 -2
- package/dist/core/db.d.ts +5 -3
- package/dist/core/db.js +81 -34
- package/dist/core/memory-read.js +31 -19
- package/dist/core/memory-write.js +54 -27
- package/dist/core/relationships.js +5 -16
- package/dist/core/search.js +60 -54
- package/dist/index.js +71 -30
- package/dist/instructions.md +5 -2
- package/dist/protocol-version-guard.d.ts +3 -0
- package/dist/protocol-version-guard.js +32 -21
- package/dist/stdio-transport.d.ts +3 -0
- package/dist/stdio-transport.js +46 -34
- package/dist/tools.js +129 -118
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
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
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
let timeout;
|
|
71
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
72
|
+
timeout = setTimeout(() => {
|
|
73
|
+
reject(new Error(message));
|
|
74
|
+
}, ms);
|
|
75
|
+
});
|
|
74
76
|
try {
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
211
|
-
return value;
|
|
207
|
+
if (typeof value === 'number') {
|
|
208
|
+
return assertFiniteNumber(value, field);
|
|
209
|
+
}
|
|
212
210
|
if (typeof value === 'bigint') {
|
|
213
|
-
|
|
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
|
+
};
|
package/dist/core/memory-read.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
package/dist/core/search.js
CHANGED
|
@@ -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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
177
|
-
}
|
|
178
|
-
if (depth === 0) {
|
|
179
|
-
return { memories: searchResults, relationships: [], depth };
|
|
193
|
+
return emptyRecallResult(getRecallDepth(input.depth));
|
|
180
194
|
}
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
36
|
-
const
|
|
37
|
-
|
|
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 (
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
89
|
-
|
|
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, () =>
|
|
141
|
+
process.on(signal, () => {
|
|
142
|
+
shutdown(signal);
|
|
143
|
+
});
|
|
103
144
|
}
|
|
104
145
|
};
|
|
105
146
|
const registerProcessHandlers = () => {
|
package/dist/instructions.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
}
|
package/dist/stdio-transport.js
CHANGED
|
@@ -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(
|
|
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
|
|
60
|
-
|
|
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
|
-
|
|
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) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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