@j0hanz/memdb 1.1.5 → 1.2.2
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/dist/config.d.ts +3 -1
- package/dist/config.js +27 -3
- package/dist/core/db.d.ts +1 -1
- package/dist/core/db.js +11 -0
- package/dist/core/memory-read.js +18 -16
- package/dist/core/memory-write.d.ts +5 -1
- package/dist/core/memory-write.js +33 -22
- package/dist/core/relationships.d.ts +15 -0
- package/dist/core/relationships.js +93 -0
- package/dist/core/search.d.ts +5 -1
- package/dist/core/search.js +93 -7
- package/dist/index.js +8 -4
- package/dist/protocol-version-guard.js +16 -0
- package/dist/schemas.d.ts +44 -0
- package/dist/schemas.js +58 -0
- package/dist/stdio-transport.d.ts +29 -0
- package/dist/stdio-transport.js +250 -0
- package/dist/tools.d.ts +11 -2
- package/dist/tools.js +101 -3
- package/dist/types.d.ts +29 -5
- package/dist/types.js +10 -1
- package/package.json +3 -3
- package/dist/core/database-schema.d.ts +0 -2
- package/dist/core/database-schema.d.ts.map +0 -1
- package/dist/core/database-schema.js +0 -64
- package/dist/core/database-schema.js.map +0 -1
- package/dist/core/database.d.ts +0 -3
- package/dist/core/database.d.ts.map +0 -1
- package/dist/core/database.js +0 -43
- package/dist/core/database.js.map +0 -1
- package/dist/core/memory-create.d.ts +0 -7
- package/dist/core/memory-create.d.ts.map +0 -1
- package/dist/core/memory-create.js +0 -40
- package/dist/core/memory-create.js.map +0 -1
- package/dist/core/memory-db.d.ts +0 -2
- package/dist/core/memory-db.d.ts.map +0 -1
- package/dist/core/memory-db.js +0 -31
- package/dist/core/memory-db.js.map +0 -1
- package/dist/core/memory-read.d.ts.map +0 -1
- package/dist/core/memory-read.js.map +0 -1
- package/dist/core/memory-relations.d.ts +0 -10
- package/dist/core/memory-relations.d.ts.map +0 -1
- package/dist/core/memory-relations.js +0 -57
- package/dist/core/memory-relations.js.map +0 -1
- package/dist/core/memory-search.d.ts +0 -10
- package/dist/core/memory-search.d.ts.map +0 -1
- package/dist/core/memory-search.js +0 -23
- package/dist/core/memory-search.js.map +0 -1
- package/dist/core/memory-stats.d.ts +0 -2
- package/dist/core/memory-stats.d.ts.map +0 -1
- package/dist/core/memory-stats.js +0 -51
- package/dist/core/memory-stats.js.map +0 -1
- package/dist/core/memory-updates.d.ts +0 -10
- package/dist/core/memory-updates.d.ts.map +0 -1
- package/dist/core/memory-updates.js +0 -115
- package/dist/core/memory-updates.js.map +0 -1
- package/dist/core/relation-queries.d.ts +0 -7
- package/dist/core/relation-queries.d.ts.map +0 -1
- package/dist/core/relation-queries.js +0 -125
- package/dist/core/relation-queries.js.map +0 -1
- package/dist/core/relations.d.ts +0 -10
- package/dist/core/relations.js +0 -177
- package/dist/core/row-mappers.d.ts +0 -6
- package/dist/core/row-mappers.d.ts.map +0 -1
- package/dist/core/row-mappers.js +0 -52
- package/dist/core/row-mappers.js.map +0 -1
- package/dist/core/search-errors.d.ts +0 -1
- package/dist/core/search-errors.d.ts.map +0 -1
- package/dist/core/search-errors.js +0 -30
- package/dist/core/search-errors.js.map +0 -1
- package/dist/core/search.d.ts.map +0 -1
- package/dist/core/search.js.map +0 -1
- package/dist/core/sqlite.d.ts +0 -10
- package/dist/core/sqlite.d.ts.map +0 -1
- package/dist/core/sqlite.js +0 -71
- package/dist/core/sqlite.js.map +0 -1
- package/dist/core/tags.d.ts +0 -1
- package/dist/core/tags.d.ts.map +0 -1
- package/dist/core/tags.js +0 -27
- package/dist/core/tags.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/errors.d.ts +0 -19
- package/dist/lib/errors.d.ts.map +0 -1
- package/dist/lib/errors.js +0 -22
- package/dist/lib/errors.js.map +0 -1
- package/dist/schemas/inputs.d.ts +0 -44
- package/dist/schemas/inputs.d.ts.map +0 -1
- package/dist/schemas/inputs.js +0 -97
- package/dist/schemas/inputs.js.map +0 -1
- package/dist/schemas/outputs.d.ts +0 -9
- package/dist/schemas/outputs.d.ts.map +0 -1
- package/dist/schemas/outputs.js +0 -28
- package/dist/schemas/outputs.js.map +0 -1
- package/dist/tools/definitions/memory-core.d.ts +0 -2
- package/dist/tools/definitions/memory-core.d.ts.map +0 -1
- package/dist/tools/definitions/memory-core.js +0 -79
- package/dist/tools/definitions/memory-core.js.map +0 -1
- package/dist/tools/definitions/memory-relations.d.ts +0 -2
- package/dist/tools/definitions/memory-relations.d.ts.map +0 -1
- package/dist/tools/definitions/memory-relations.js +0 -43
- package/dist/tools/definitions/memory-relations.js.map +0 -1
- package/dist/tools/definitions/memory-search.d.ts +0 -2
- package/dist/tools/definitions/memory-search.d.ts.map +0 -1
- package/dist/tools/definitions/memory-search.js +0 -20
- package/dist/tools/definitions/memory-search.js.map +0 -1
- package/dist/tools/definitions/memory-stats.d.ts +0 -2
- package/dist/tools/definitions/memory-stats.d.ts.map +0 -1
- package/dist/tools/definitions/memory-stats.js +0 -20
- package/dist/tools/definitions/memory-stats.js.map +0 -1
- package/dist/tools/index.d.ts +0 -2
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -15
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/tool-handlers.d.ts +0 -3
- package/dist/tools/tool-handlers.d.ts.map +0 -1
- package/dist/tools/tool-handlers.js +0 -19
- package/dist/tools/tool-handlers.js.map +0 -1
- package/dist/tools/tool-types.d.ts +0 -14
- package/dist/tools/tool-types.d.ts.map +0 -1
- package/dist/tools/tool-types.js +0 -1
- package/dist/tools/tool-types.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/types/index.d.ts +0 -37
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/config.d.ts +0 -6
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js +0 -99
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -5
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -20
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils.d.ts +0 -11
- package/dist/utils.js +0 -118
- package/dist/worker/db-worker-client.d.ts +0 -9
- package/dist/worker/db-worker-client.js +0 -93
- package/dist/worker/db-worker.d.ts +0 -1
- package/dist/worker/db-worker.js +0 -174
- package/dist/worker/protocol.d.ts +0 -9
- package/dist/worker/protocol.js +0 -14
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -2,12 +2,36 @@ import path from 'node:path';
|
|
|
2
2
|
import process from 'node:process';
|
|
3
3
|
const DEFAULT_DB_PATH = path.join(process.cwd(), '.memdb', 'memory.db');
|
|
4
4
|
const DEFAULT_LOG_LEVEL = 'info';
|
|
5
|
+
const hasNullByte = (value) => value.includes('\0');
|
|
6
|
+
const parseLogLevel = (value) => {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (hasNullByte(value)) {
|
|
10
|
+
throw new Error('Invalid MEMDB_LOG_LEVEL: null byte detected');
|
|
11
|
+
}
|
|
12
|
+
switch (value) {
|
|
13
|
+
case 'error':
|
|
14
|
+
case 'info':
|
|
15
|
+
case 'warn':
|
|
16
|
+
return value;
|
|
17
|
+
default:
|
|
18
|
+
throw new Error(`Invalid MEMDB_LOG_LEVEL: ${value} (expected: error|warn|info)`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
5
21
|
const resolveDbPath = (env) => {
|
|
6
|
-
|
|
7
|
-
|
|
22
|
+
const envPath = env.MEMDB_PATH;
|
|
23
|
+
if (envPath !== undefined) {
|
|
24
|
+
if (hasNullByte(envPath)) {
|
|
25
|
+
throw new Error('Invalid MEMDB_PATH: null byte detected');
|
|
26
|
+
}
|
|
27
|
+
if (envPath === ':memory:')
|
|
28
|
+
return ':memory:';
|
|
29
|
+
if (envPath.length > 0)
|
|
30
|
+
return path.resolve(envPath);
|
|
31
|
+
}
|
|
8
32
|
return path.resolve(DEFAULT_DB_PATH);
|
|
9
33
|
};
|
|
10
34
|
export const config = {
|
|
11
35
|
dbPath: resolveDbPath(process.env),
|
|
12
|
-
logLevel: DEFAULT_LOG_LEVEL,
|
|
36
|
+
logLevel: parseLogLevel(process.env.MEMDB_LOG_LEVEL) ?? DEFAULT_LOG_LEVEL,
|
|
13
37
|
};
|
package/dist/core/db.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync, type StatementSync } from 'node:sqlite';
|
|
2
|
-
import type
|
|
2
|
+
import { type Memory, 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;
|
package/dist/core/db.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { DatabaseSync } from 'node:sqlite';
|
|
4
4
|
import { config } from '../config.js';
|
|
5
|
+
import { MEMORY_TYPES, } from '../types.js';
|
|
5
6
|
const SCHEMA_SQL = `
|
|
6
7
|
PRAGMA journal_mode = WAL;
|
|
7
8
|
PRAGMA synchronous = NORMAL;
|
|
@@ -227,6 +228,14 @@ const toString = (value, field) => {
|
|
|
227
228
|
return value;
|
|
228
229
|
throw createFieldError(field);
|
|
229
230
|
};
|
|
231
|
+
const isMemoryType = (value) => MEMORY_TYPES.includes(value);
|
|
232
|
+
const toMemoryType = (value, field) => {
|
|
233
|
+
const str = toString(value, field);
|
|
234
|
+
if (!isMemoryType(str)) {
|
|
235
|
+
throw createFieldError(field);
|
|
236
|
+
}
|
|
237
|
+
return str;
|
|
238
|
+
};
|
|
230
239
|
const toOptionalString = (value, field) => {
|
|
231
240
|
if (value === null || value === undefined)
|
|
232
241
|
return undefined;
|
|
@@ -241,6 +250,8 @@ export const mapRowToMemory = (row) => ({
|
|
|
241
250
|
id: toSafeInteger(row.id, 'id'),
|
|
242
251
|
content: toString(row.content, 'content'),
|
|
243
252
|
summary: toOptionalString(row.summary, 'summary'),
|
|
253
|
+
importance: toSafeInteger(row.importance ?? 0, 'importance'),
|
|
254
|
+
memory_type: toMemoryType(row.memory_type ?? 'general', 'memory_type'),
|
|
244
255
|
created_at: toString(row.created_at, 'created_at'),
|
|
245
256
|
accessed_at: toString(row.accessed_at, 'accessed_at'),
|
|
246
257
|
hash: toString(row.hash, 'hash'),
|
package/dist/core/memory-read.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { db, executeGet, executeRun, mapRowToMemory, toSafeInteger, } from './db.js';
|
|
1
|
+
import { db, executeGet, executeRun, mapRowToMemory, toSafeInteger, withImmediateTransaction, } from './db.js';
|
|
2
2
|
const stmtGetMemoryByHash = db.prepare('SELECT * FROM memories WHERE hash = ?');
|
|
3
3
|
const stmtDeleteMemoryByHash = db.prepare('DELETE FROM memories WHERE hash = ?');
|
|
4
4
|
export const getMemory = (hash) => {
|
|
@@ -13,25 +13,27 @@ export const deleteMemories = (hashes) => {
|
|
|
13
13
|
const results = [];
|
|
14
14
|
let succeeded = 0;
|
|
15
15
|
let failed = 0;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
return withImmediateTransaction(() => {
|
|
17
|
+
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
|
+
}
|
|
22
28
|
}
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
catch (err) {
|
|
30
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
31
|
+
results.push({ hash, deleted: false, error: message });
|
|
25
32
|
failed++;
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
results.push({ hash, deleted: false, error: message });
|
|
31
|
-
failed++;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return { results, succeeded, failed };
|
|
35
|
+
return { results, succeeded, failed };
|
|
36
|
+
});
|
|
35
37
|
};
|
|
36
38
|
const stmtMemoryCount = db.prepare('SELECT COUNT(*) as count FROM memories');
|
|
37
39
|
const stmtTagCount = db.prepare('SELECT COUNT(DISTINCT tag) as count FROM tags');
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import type { BatchStoreResult, MemoryInsertResult, MemoryUpdateResult } from '../types.js';
|
|
1
|
+
import type { BatchStoreResult, MemoryInsertResult, MemoryType, MemoryUpdateResult } from '../types.js';
|
|
2
2
|
export declare const createMemory: (input: {
|
|
3
3
|
content: string;
|
|
4
4
|
tags?: readonly string[];
|
|
5
|
+
importance?: number;
|
|
6
|
+
memory_type?: MemoryType;
|
|
5
7
|
}) => MemoryInsertResult;
|
|
6
8
|
export declare const createMemories: (items: {
|
|
7
9
|
content: string;
|
|
8
10
|
tags?: readonly string[];
|
|
11
|
+
importance?: number;
|
|
12
|
+
memory_type?: MemoryType;
|
|
9
13
|
}[]) => BatchStoreResult;
|
|
10
14
|
interface UpdateMemoryOptions {
|
|
11
15
|
content: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { db, executeGet, executeRun, toSafeInteger, withImmediateTransaction, } from './db.js';
|
|
3
3
|
const MAX_TAGS = 100;
|
|
4
|
+
const TAG_PATTERN = /^\S+$/;
|
|
4
5
|
const validateTag = (tag) => {
|
|
5
6
|
if (tag.length === 0) {
|
|
6
7
|
throw new Error('Tag must be at least 1 character');
|
|
@@ -8,6 +9,9 @@ const validateTag = (tag) => {
|
|
|
8
9
|
if (tag.length > 50) {
|
|
9
10
|
throw new Error('Tag exceeds 50 characters');
|
|
10
11
|
}
|
|
12
|
+
if (!TAG_PATTERN.test(tag)) {
|
|
13
|
+
throw new Error('Tag must not contain whitespace');
|
|
14
|
+
}
|
|
11
15
|
};
|
|
12
16
|
const validateTagCount = (tags, maxTags) => {
|
|
13
17
|
if (tags.length > maxTags) {
|
|
@@ -60,49 +64,56 @@ const buildHash = (content) => {
|
|
|
60
64
|
// eslint-disable-next-line sonarjs/hashing -- MD5 used for non-security deduplication only.
|
|
61
65
|
return crypto.createHash('md5').update(content).digest('hex');
|
|
62
66
|
};
|
|
63
|
-
const stmtInsertMemory = db.prepare('INSERT OR IGNORE INTO memories (content, hash) VALUES (?, ?) RETURNING id');
|
|
67
|
+
const stmtInsertMemory = db.prepare('INSERT OR IGNORE INTO memories (content, hash, importance, memory_type) VALUES (?, ?, ?, ?) RETURNING id');
|
|
64
68
|
const requireMemoryId = (id) => {
|
|
65
69
|
if (id === undefined) {
|
|
66
70
|
throw new Error('Failed to resolve memory id');
|
|
67
71
|
}
|
|
68
72
|
return id;
|
|
69
73
|
};
|
|
70
|
-
const resolveMemoryId = (content, hash) => {
|
|
71
|
-
const inserted = executeGet(stmtInsertMemory, content, hash);
|
|
74
|
+
const resolveMemoryId = (content, hash, importance, memoryType) => {
|
|
75
|
+
const inserted = executeGet(stmtInsertMemory, content, hash, importance, memoryType);
|
|
72
76
|
if (inserted) {
|
|
73
77
|
return { id: toSafeInteger(inserted.id, 'id'), isNew: true };
|
|
74
78
|
}
|
|
75
79
|
const id = requireMemoryId(findMemoryIdByHash(hash));
|
|
76
80
|
return { id, isNew: false };
|
|
77
81
|
};
|
|
78
|
-
export const createMemory = (input) => withImmediateTransaction(() =>
|
|
79
|
-
|
|
82
|
+
export const createMemory = (input) => withImmediateTransaction(() => createMemoryInTransaction(input));
|
|
83
|
+
const createMemoryInTransaction = (input) => {
|
|
84
|
+
const { content, tags = [], importance = 0, memory_type: memoryType = 'general', } = input;
|
|
80
85
|
const hash = buildHash(content);
|
|
81
86
|
const normalizedTags = normalizeTags(tags, MAX_TAGS);
|
|
82
|
-
const { id, isNew } = resolveMemoryId(content, hash);
|
|
87
|
+
const { id, isNew } = resolveMemoryId(content, hash, importance, memoryType);
|
|
83
88
|
insertTags(id, normalizedTags);
|
|
84
89
|
return { id, hash, isNew };
|
|
85
|
-
}
|
|
90
|
+
};
|
|
86
91
|
export const createMemories = (items) => {
|
|
87
92
|
const results = [];
|
|
88
93
|
let succeeded = 0;
|
|
89
94
|
let failed = 0;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|
|
103
114
|
}
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
return { results, succeeded, failed };
|
|
116
|
+
});
|
|
106
117
|
};
|
|
107
118
|
const stmtDeleteTagsForMemory = db.prepare('DELETE FROM tags WHERE memory_id = ?');
|
|
108
119
|
const stmtUpdateContent = db.prepare('UPDATE memories SET content = ?, hash = ? WHERE id = ?');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CreateRelationshipResult, Relationship, StatementResult } from '../types.js';
|
|
2
|
+
export declare const createRelationship: (input: {
|
|
3
|
+
from_hash: string;
|
|
4
|
+
to_hash: string;
|
|
5
|
+
relation_type: string;
|
|
6
|
+
}) => CreateRelationshipResult;
|
|
7
|
+
export declare const getRelationships: (input: {
|
|
8
|
+
hash: string;
|
|
9
|
+
direction?: "outgoing" | "incoming" | "both";
|
|
10
|
+
}) => Relationship[];
|
|
11
|
+
export declare const deleteRelationship: (input: {
|
|
12
|
+
from_hash: string;
|
|
13
|
+
to_hash: string;
|
|
14
|
+
relation_type: string;
|
|
15
|
+
}) => StatementResult;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { db, executeAll, executeGet, executeRun, toSafeInteger, withImmediateTransaction, } from './db.js';
|
|
2
|
+
const stmtFindMemoryIdByHash = db.prepare('SELECT id FROM memories WHERE hash = ?');
|
|
3
|
+
const findMemoryIdByHash = (hash) => {
|
|
4
|
+
const row = executeGet(stmtFindMemoryIdByHash, hash);
|
|
5
|
+
if (!row)
|
|
6
|
+
return undefined;
|
|
7
|
+
return toSafeInteger(row.id, 'id');
|
|
8
|
+
};
|
|
9
|
+
const requireMemoryId = (hash) => {
|
|
10
|
+
const id = findMemoryIdByHash(hash);
|
|
11
|
+
if (id === undefined) {
|
|
12
|
+
throw new Error(`Memory not found: ${hash}`);
|
|
13
|
+
}
|
|
14
|
+
return id;
|
|
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
|
+
const stmtInsertRelationship = db.prepare(`
|
|
29
|
+
INSERT OR IGNORE INTO relationships (from_memory_id, to_memory_id, relation_type)
|
|
30
|
+
VALUES (?, ?, ?)
|
|
31
|
+
RETURNING id
|
|
32
|
+
`);
|
|
33
|
+
const stmtFindRelationshipId = db.prepare(`
|
|
34
|
+
SELECT id FROM relationships
|
|
35
|
+
WHERE from_memory_id = ? AND to_memory_id = ? AND relation_type = ?
|
|
36
|
+
`);
|
|
37
|
+
export const createRelationship = (input) => withImmediateTransaction(() => {
|
|
38
|
+
const fromId = requireMemoryId(input.from_hash);
|
|
39
|
+
const toId = requireMemoryId(input.to_hash);
|
|
40
|
+
if (fromId === toId) {
|
|
41
|
+
throw new Error('Cannot create self-referential relationship');
|
|
42
|
+
}
|
|
43
|
+
const inserted = executeGet(stmtInsertRelationship, fromId, toId, input.relation_type);
|
|
44
|
+
if (inserted) {
|
|
45
|
+
return { id: toSafeInteger(inserted.id, 'id'), isNew: true };
|
|
46
|
+
}
|
|
47
|
+
// Relationship already exists, find its ID
|
|
48
|
+
const existing = executeGet(stmtFindRelationshipId, fromId, toId, input.relation_type);
|
|
49
|
+
if (!existing) {
|
|
50
|
+
throw new Error('Failed to resolve relationship id');
|
|
51
|
+
}
|
|
52
|
+
return { id: toSafeInteger(existing.id, 'id'), isNew: false };
|
|
53
|
+
});
|
|
54
|
+
// Query that joins with memories to get hashes instead of IDs
|
|
55
|
+
const buildGetRelationshipsQuery = (direction) => {
|
|
56
|
+
const baseSelect = `
|
|
57
|
+
SELECT r.id, r.relation_type, r.created_at,
|
|
58
|
+
mf.hash as from_hash, mt.hash as to_hash
|
|
59
|
+
FROM relationships r
|
|
60
|
+
JOIN memories mf ON r.from_memory_id = mf.id
|
|
61
|
+
JOIN memories mt ON r.to_memory_id = mt.id
|
|
62
|
+
`;
|
|
63
|
+
switch (direction) {
|
|
64
|
+
case 'outgoing':
|
|
65
|
+
return `${baseSelect} WHERE mf.hash = ?`;
|
|
66
|
+
case 'incoming':
|
|
67
|
+
return `${baseSelect} WHERE mt.hash = ?`;
|
|
68
|
+
case 'both':
|
|
69
|
+
return `${baseSelect} WHERE mf.hash = ? OR mt.hash = ?`;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const stmtGetRelationships = {
|
|
73
|
+
outgoing: db.prepare(buildGetRelationshipsQuery('outgoing')),
|
|
74
|
+
incoming: db.prepare(buildGetRelationshipsQuery('incoming')),
|
|
75
|
+
both: db.prepare(buildGetRelationshipsQuery('both')),
|
|
76
|
+
};
|
|
77
|
+
export const getRelationships = (input) => {
|
|
78
|
+
const direction = input.direction ?? 'both';
|
|
79
|
+
const stmt = stmtGetRelationships[direction];
|
|
80
|
+
const params = direction === 'both' ? [input.hash, input.hash] : [input.hash];
|
|
81
|
+
const rows = executeAll(stmt, ...params);
|
|
82
|
+
return rows.map(mapRowToRelationship);
|
|
83
|
+
};
|
|
84
|
+
const stmtDeleteRelationship = db.prepare(`
|
|
85
|
+
DELETE FROM relationships
|
|
86
|
+
WHERE from_memory_id = (SELECT id FROM memories WHERE hash = ?)
|
|
87
|
+
AND to_memory_id = (SELECT id FROM memories WHERE hash = ?)
|
|
88
|
+
AND relation_type = ?
|
|
89
|
+
`);
|
|
90
|
+
export const deleteRelationship = (input) => {
|
|
91
|
+
const result = executeRun(stmtDeleteRelationship, input.from_hash, input.to_hash, input.relation_type);
|
|
92
|
+
return { changes: toSafeInteger(result.changes, 'changes') };
|
|
93
|
+
};
|
package/dist/core/search.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { SearchResult } from '../types.js';
|
|
1
|
+
import type { RecallResult, SearchResult } from '../types.js';
|
|
2
2
|
interface SearchInput {
|
|
3
3
|
query: string;
|
|
4
4
|
}
|
|
5
5
|
export declare const searchMemories: (input: SearchInput) => SearchResult[];
|
|
6
|
+
export declare const recallMemories: (input: {
|
|
7
|
+
query: string;
|
|
8
|
+
depth?: number;
|
|
9
|
+
}) => RecallResult;
|
|
6
10
|
export {};
|
package/dist/core/search.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { executeAll, mapRowToSearchResult, prepareCached, } from './db.js';
|
|
1
|
+
import { db, executeAll, mapRowToSearchResult, prepareCached, toSafeInteger, } from './db.js';
|
|
2
2
|
const MAX_QUERY_TOKENS = 50;
|
|
3
3
|
const DEFAULT_LIMIT = 100;
|
|
4
|
+
const RECENCY_DECAY_DAYS = 7;
|
|
5
|
+
const RECENCY_WEIGHT = 0.15;
|
|
4
6
|
const tokenizeQuery = (query) => {
|
|
5
7
|
const parts = query
|
|
6
8
|
.trim()
|
|
@@ -22,31 +24,31 @@ const buildFtsQuery = (tokens) => {
|
|
|
22
24
|
const buildTagPlaceholders = (count) => {
|
|
23
25
|
return Array.from({ length: count }, () => '?').join(', ');
|
|
24
26
|
};
|
|
25
|
-
// Search both content (FTS) and tags, deduplicated by memory id
|
|
26
27
|
const buildSearchQuery = (tokens) => {
|
|
27
28
|
const ftsQuery = buildFtsQuery(tokens);
|
|
28
29
|
const tagPlaceholders = buildTagPlaceholders(tokens.length);
|
|
29
30
|
const relevanceExpr = '1.0 / (1.0 + abs(bm25(memories_fts)))';
|
|
31
|
+
const recencyBoost = `MAX(0.0, (${RECENCY_DECAY_DAYS}.0 - julianday('now') + julianday(created_at)) / ${RECENCY_DECAY_DAYS}.0) * ${RECENCY_WEIGHT}`;
|
|
30
32
|
// Union of FTS content matches and tag matches, deduplicated
|
|
31
33
|
const sql = `
|
|
32
34
|
WITH content_matches AS (
|
|
33
|
-
SELECT m.*, ${relevanceExpr} as
|
|
35
|
+
SELECT m.*, ${relevanceExpr} as base_relevance, ${recencyBoost} as recency_bonus
|
|
34
36
|
FROM memories m
|
|
35
37
|
JOIN memories_fts ON m.id = memories_fts.rowid
|
|
36
38
|
WHERE memories_fts MATCH ?
|
|
37
39
|
),
|
|
38
40
|
tag_matches AS (
|
|
39
|
-
SELECT DISTINCT m.*, 0.5 as
|
|
41
|
+
SELECT DISTINCT m.*, 0.5 as base_relevance, ${recencyBoost} as recency_bonus
|
|
40
42
|
FROM memories m
|
|
41
43
|
JOIN tags t ON m.id = t.memory_id
|
|
42
44
|
WHERE t.tag IN (${tagPlaceholders})
|
|
43
45
|
),
|
|
44
46
|
combined AS (
|
|
45
|
-
SELECT
|
|
47
|
+
SELECT *, (base_relevance + recency_bonus) as relevance FROM content_matches
|
|
46
48
|
UNION ALL
|
|
47
|
-
SELECT
|
|
49
|
+
SELECT *, (base_relevance + recency_bonus) as relevance FROM tag_matches
|
|
48
50
|
)
|
|
49
|
-
SELECT id, content, summary, created_at, accessed_at, hash,
|
|
51
|
+
SELECT id, content, summary, importance, memory_type, created_at, accessed_at, hash,
|
|
50
52
|
MAX(relevance) as relevance
|
|
51
53
|
FROM combined
|
|
52
54
|
GROUP BY id
|
|
@@ -107,3 +109,87 @@ export const searchMemories = (input) => {
|
|
|
107
109
|
const rows = executeSearch(sql, params);
|
|
108
110
|
return rows.map((row) => mapRowToSearchResult(row));
|
|
109
111
|
};
|
|
112
|
+
const MAX_RECALL_DEPTH = 3;
|
|
113
|
+
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
|
+
const buildRecallQuery = (seedCount, depth) => {
|
|
127
|
+
const seedPlaceholders = Array.from({ length: seedCount }, () => '?').join(', ');
|
|
128
|
+
const safeDepth = Math.min(Math.max(0, depth), MAX_RECALL_DEPTH);
|
|
129
|
+
const sql = `
|
|
130
|
+
WITH RECURSIVE connected(memory_id, depth) AS (
|
|
131
|
+
-- Seed memories from search results
|
|
132
|
+
SELECT id, 0 FROM memories WHERE id IN (${seedPlaceholders})
|
|
133
|
+
UNION
|
|
134
|
+
-- Follow relationships (both directions) up to max depth
|
|
135
|
+
SELECT
|
|
136
|
+
CASE
|
|
137
|
+
WHEN r.from_memory_id = c.memory_id THEN r.to_memory_id
|
|
138
|
+
ELSE r.from_memory_id
|
|
139
|
+
END,
|
|
140
|
+
c.depth + 1
|
|
141
|
+
FROM relationships r
|
|
142
|
+
JOIN connected c ON (r.from_memory_id = c.memory_id OR r.to_memory_id = c.memory_id)
|
|
143
|
+
WHERE c.depth < ${safeDepth}
|
|
144
|
+
),
|
|
145
|
+
unique_memories AS (
|
|
146
|
+
SELECT DISTINCT memory_id, MIN(depth) as min_depth
|
|
147
|
+
FROM connected
|
|
148
|
+
GROUP BY memory_id
|
|
149
|
+
ORDER BY min_depth
|
|
150
|
+
LIMIT ${MAX_RECALL_MEMORIES}
|
|
151
|
+
)
|
|
152
|
+
SELECT m.*, 1.0 / (1.0 + um.min_depth) as relevance
|
|
153
|
+
FROM memories m
|
|
154
|
+
JOIN unique_memories um ON m.id = um.memory_id
|
|
155
|
+
ORDER BY um.min_depth, m.created_at DESC
|
|
156
|
+
`;
|
|
157
|
+
return { sql };
|
|
158
|
+
};
|
|
159
|
+
const buildRelationshipsQuery = (memoryCount) => {
|
|
160
|
+
const placeholders = Array.from({ length: memoryCount }, () => '?').join(', ');
|
|
161
|
+
const sql = `
|
|
162
|
+
SELECT r.id, r.relation_type, r.created_at,
|
|
163
|
+
mf.hash as from_hash, mt.hash as to_hash
|
|
164
|
+
FROM relationships r
|
|
165
|
+
JOIN memories mf ON r.from_memory_id = mf.id
|
|
166
|
+
JOIN memories mt ON r.to_memory_id = mt.id
|
|
167
|
+
WHERE r.from_memory_id IN (${placeholders})
|
|
168
|
+
AND r.to_memory_id IN (${placeholders})
|
|
169
|
+
`;
|
|
170
|
+
return { sql };
|
|
171
|
+
};
|
|
172
|
+
export const recallMemories = (input) => {
|
|
173
|
+
const depth = input.depth ?? 1;
|
|
174
|
+
const searchResults = searchMemories({ query: input.query });
|
|
175
|
+
if (searchResults.length === 0) {
|
|
176
|
+
return { memories: [], relationships: [], depth };
|
|
177
|
+
}
|
|
178
|
+
if (depth === 0) {
|
|
179
|
+
return { memories: searchResults, relationships: [], depth };
|
|
180
|
+
}
|
|
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 };
|
|
189
|
+
}
|
|
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 };
|
|
195
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
5
|
import { SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
6
|
import { closeDb } from './core/db.js';
|
|
8
7
|
import { logger } from './logger.js';
|
|
9
8
|
import { ProtocolVersionGuardTransport } from './protocol-version-guard.js';
|
|
9
|
+
import { BatchRejectingStdioServerTransport } from './stdio-transport.js';
|
|
10
10
|
import { registerAllTools } from './tools.js';
|
|
11
11
|
const readPackageVersion = async () => {
|
|
12
12
|
const packageJsonText = await readFile(new URL('../package.json', import.meta.url), {
|
|
@@ -25,8 +25,11 @@ const server = new McpServer({ name: 'memdb', version: packageVersion ?? '0.0.0'
|
|
|
25
25
|
capabilities: { tools: {} },
|
|
26
26
|
});
|
|
27
27
|
const patchToolErrorResults = (target) => {
|
|
28
|
-
const
|
|
29
|
-
|
|
28
|
+
const targetUnknown = target;
|
|
29
|
+
const existing = Reflect.get(targetUnknown, 'createToolError');
|
|
30
|
+
if (existing !== undefined && typeof existing !== 'function')
|
|
31
|
+
return;
|
|
32
|
+
const createToolError = (message) => {
|
|
30
33
|
const structured = {
|
|
31
34
|
ok: false,
|
|
32
35
|
error: { code: 'E_TOOL_ERROR', message },
|
|
@@ -37,6 +40,7 @@ const patchToolErrorResults = (target) => {
|
|
|
37
40
|
isError: true,
|
|
38
41
|
};
|
|
39
42
|
};
|
|
43
|
+
Reflect.set(targetUnknown, 'createToolError', createToolError);
|
|
40
44
|
};
|
|
41
45
|
patchToolErrorResults(server);
|
|
42
46
|
registerAllTools(server);
|
|
@@ -66,7 +70,7 @@ async function shutdown(signal) {
|
|
|
66
70
|
}
|
|
67
71
|
const main = async () => {
|
|
68
72
|
try {
|
|
69
|
-
const stdioTransport = new
|
|
73
|
+
const stdioTransport = new BatchRejectingStdioServerTransport();
|
|
70
74
|
const guardedTransport = new ProtocolVersionGuardTransport(stdioTransport, SUPPORTED_PROTOCOL_VERSIONS);
|
|
71
75
|
transport = guardedTransport;
|
|
72
76
|
await server.connect(guardedTransport);
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { isInitializedNotification, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
const buildUnsupportedVersionMessage = (version, supportedVersions) => `Unsupported protocol version: ${version} (supported versions: ${supportedVersions.join(', ')})`;
|
|
3
3
|
const isObject = (value) => typeof value === 'object' && value !== null;
|
|
4
|
+
const rejectBatchIfPresent = (message, inner) => {
|
|
5
|
+
if (!Array.isArray(message))
|
|
6
|
+
return false;
|
|
7
|
+
for (const item of message) {
|
|
8
|
+
if (!isObject(item))
|
|
9
|
+
continue;
|
|
10
|
+
const id = Reflect.get(item, 'id');
|
|
11
|
+
if (typeof id !== 'string' && typeof id !== 'number')
|
|
12
|
+
continue;
|
|
13
|
+
void inner.send(createLifecycleError(id, 'Invalid request: JSON-RPC batching is not supported'), { relatedRequestId: id });
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
};
|
|
4
17
|
const getInitializeInfo = (message) => {
|
|
5
18
|
if (!isInitializeRequest(message))
|
|
6
19
|
return undefined;
|
|
@@ -64,6 +77,9 @@ export class ProtocolVersionGuardTransport {
|
|
|
64
77
|
await this.inner.close();
|
|
65
78
|
}
|
|
66
79
|
handleMessage(message, extra) {
|
|
80
|
+
if (rejectBatchIfPresent(message, this.inner)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
67
83
|
const initializeInfo = getInitializeInfo(message);
|
|
68
84
|
if (initializeInfo) {
|
|
69
85
|
if (this.sawInitialize) {
|
package/dist/schemas.d.ts
CHANGED
|
@@ -2,11 +2,33 @@ import { z } from 'zod';
|
|
|
2
2
|
export declare const StoreMemoryInputSchema: z.ZodObject<{
|
|
3
3
|
content: z.ZodString;
|
|
4
4
|
tags: z.ZodArray<z.ZodString>;
|
|
5
|
+
importance: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
memory_type: z.ZodOptional<z.ZodEnum<{
|
|
7
|
+
error: "error";
|
|
8
|
+
general: "general";
|
|
9
|
+
fact: "fact";
|
|
10
|
+
plan: "plan";
|
|
11
|
+
decision: "decision";
|
|
12
|
+
reflection: "reflection";
|
|
13
|
+
lesson: "lesson";
|
|
14
|
+
gradient: "gradient";
|
|
15
|
+
}>>;
|
|
5
16
|
}, z.core.$strict>;
|
|
6
17
|
export declare const StoreMemoriesInputSchema: z.ZodObject<{
|
|
7
18
|
items: z.ZodArray<z.ZodObject<{
|
|
8
19
|
content: z.ZodString;
|
|
9
20
|
tags: z.ZodArray<z.ZodString>;
|
|
21
|
+
importance: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
memory_type: z.ZodOptional<z.ZodEnum<{
|
|
23
|
+
error: "error";
|
|
24
|
+
general: "general";
|
|
25
|
+
fact: "fact";
|
|
26
|
+
plan: "plan";
|
|
27
|
+
decision: "decision";
|
|
28
|
+
reflection: "reflection";
|
|
29
|
+
lesson: "lesson";
|
|
30
|
+
gradient: "gradient";
|
|
31
|
+
}>>;
|
|
10
32
|
}, z.core.$strict>>;
|
|
11
33
|
}, z.core.$strict>;
|
|
12
34
|
export declare const SearchMemoriesInputSchema: z.ZodObject<{
|
|
@@ -27,6 +49,28 @@ export declare const UpdateMemoryInputSchema: z.ZodObject<{
|
|
|
27
49
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
50
|
}, z.core.$strict>;
|
|
29
51
|
export declare const MemoryStatsInputSchema: z.ZodObject<{}, z.core.$strict>;
|
|
52
|
+
export declare const CreateRelationshipInputSchema: z.ZodObject<{
|
|
53
|
+
from_hash: z.ZodString;
|
|
54
|
+
to_hash: z.ZodString;
|
|
55
|
+
relation_type: z.ZodString;
|
|
56
|
+
}, z.core.$strict>;
|
|
57
|
+
export declare const GetRelationshipsInputSchema: z.ZodObject<{
|
|
58
|
+
hash: z.ZodString;
|
|
59
|
+
direction: z.ZodOptional<z.ZodEnum<{
|
|
60
|
+
outgoing: "outgoing";
|
|
61
|
+
incoming: "incoming";
|
|
62
|
+
both: "both";
|
|
63
|
+
}>>;
|
|
64
|
+
}, z.core.$strict>;
|
|
65
|
+
export declare const DeleteRelationshipInputSchema: z.ZodObject<{
|
|
66
|
+
from_hash: z.ZodString;
|
|
67
|
+
to_hash: z.ZodString;
|
|
68
|
+
relation_type: z.ZodString;
|
|
69
|
+
}, z.core.$strict>;
|
|
70
|
+
export declare const RecallInputSchema: z.ZodObject<{
|
|
71
|
+
query: z.ZodString;
|
|
72
|
+
depth: z.ZodOptional<z.ZodNumber>;
|
|
73
|
+
}, z.core.$strict>;
|
|
30
74
|
export declare const DefaultOutputSchema: z.ZodObject<{
|
|
31
75
|
ok: z.ZodBoolean;
|
|
32
76
|
result: z.ZodOptional<z.ZodUnknown>;
|