@j0hanz/memdb 1.2.9 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +663 -382
- package/dist/assets/logo.svg +12 -0
- package/dist/async-context.d.ts +6 -0
- package/dist/async-context.js +4 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +10 -5
- package/dist/core/abort.d.ts +1 -0
- package/dist/core/abort.js +3 -0
- package/dist/core/db.d.ts +9 -5
- package/dist/core/db.js +205 -129
- package/dist/core/memory-read.d.ts +1 -2
- package/dist/core/memory-read.js +18 -21
- package/dist/core/memory-write.d.ts +1 -2
- package/dist/core/memory-write.js +69 -85
- package/dist/core/relationships.d.ts +0 -1
- package/dist/core/relationships.js +56 -59
- package/dist/core/search.d.ts +0 -1
- package/dist/core/search.js +141 -85
- package/dist/error-utils.d.ts +1 -0
- package/dist/error-utils.js +28 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +63 -16
- package/dist/instructions.md +38 -34
- package/dist/logger.d.ts +0 -1
- package/dist/logger.js +6 -3
- package/dist/protocol-version-guard.d.ts +0 -1
- package/dist/protocol-version-guard.js +17 -2
- package/dist/schemas.d.ts +0 -1
- package/dist/schemas.js +12 -9
- package/dist/stdio-transport.d.ts +0 -1
- package/dist/stdio-transport.js +6 -16
- package/dist/tools.d.ts +1 -2
- package/dist/tools.js +222 -222
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/package.json +20 -18
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/core/db.d.ts.map +0 -1
- package/dist/core/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-write.d.ts.map +0 -1
- package/dist/core/memory-write.js.map +0 -1
- package/dist/core/relationships.d.ts.map +0 -1
- package/dist/core/relationships.js.map +0 -1
- package/dist/core/search.d.ts.map +0 -1
- package/dist/core/search.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/protocol-version-guard.d.ts.map +0 -1
- package/dist/protocol-version-guard.js.map +0 -1
- package/dist/schemas.d.ts.map +0 -1
- package/dist/schemas.js.map +0 -1
- package/dist/stdio-transport.d.ts.map +0 -1
- package/dist/stdio-transport.js.map +0 -1
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
1
2
|
import crypto from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
+
import { throwIfAborted } from './abort.js';
|
|
4
|
+
import { findMemoryIdByHash, sqlGet, sqlRun, toSafeInteger, withImmediateTransaction, withSavepoint, } from './db.js';
|
|
3
5
|
const MAX_TAGS = 100;
|
|
4
6
|
const TAG_PATTERN = /^\S+$/;
|
|
7
|
+
const TAG_CASE_FOLD_LOCALE = 'en-US';
|
|
8
|
+
const normalizeTagCase = (tag) => tag.normalize('NFKC').toLocaleLowerCase(TAG_CASE_FOLD_LOCALE);
|
|
5
9
|
const validateTag = (tag) => {
|
|
6
|
-
if (tag.length === 0)
|
|
10
|
+
if (tag.length === 0)
|
|
7
11
|
throw new Error('Tag must be at least 1 character');
|
|
8
|
-
|
|
9
|
-
if (tag.length > 50) {
|
|
12
|
+
if (tag.length > 50)
|
|
10
13
|
throw new Error('Tag exceeds 50 characters');
|
|
11
|
-
|
|
12
|
-
if (!TAG_PATTERN.test(tag)) {
|
|
14
|
+
if (!TAG_PATTERN.test(tag))
|
|
13
15
|
throw new Error('Tag must not contain whitespace');
|
|
14
|
-
}
|
|
15
16
|
};
|
|
16
17
|
const validateTagCount = (tags, maxTags) => {
|
|
17
18
|
if (tags.length > maxTags) {
|
|
@@ -21,8 +22,9 @@ const validateTagCount = (tags, maxTags) => {
|
|
|
21
22
|
const dedupeTags = (tags) => {
|
|
22
23
|
const seen = new Set();
|
|
23
24
|
for (const tag of tags) {
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const normalized = normalizeTagCase(tag);
|
|
26
|
+
validateTag(normalized);
|
|
27
|
+
seen.add(normalized);
|
|
26
28
|
}
|
|
27
29
|
return [...seen];
|
|
28
30
|
};
|
|
@@ -32,102 +34,85 @@ const normalizeTags = (tags, maxTags) => {
|
|
|
32
34
|
validateTagCount(tags, maxTags);
|
|
33
35
|
return dedupeTags(tags);
|
|
34
36
|
};
|
|
35
|
-
const findMemoryIdByHash = (hash) => {
|
|
36
|
-
const stmtFindMemoryIdByHash = prepareCached('SELECT id FROM memories WHERE hash = ?');
|
|
37
|
-
const row = executeGet(stmtFindMemoryIdByHash, hash);
|
|
38
|
-
if (!row)
|
|
39
|
-
return undefined;
|
|
40
|
-
return toSafeInteger(row.id, 'id');
|
|
41
|
-
};
|
|
42
37
|
const insertTags = (memoryId, tags) => {
|
|
43
38
|
if (tags.length === 0)
|
|
44
39
|
return;
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
const insertResult = sqlRun `
|
|
41
|
+
INSERT OR IGNORE INTO tags (memory_id, tag)
|
|
42
|
+
SELECT ${memoryId}, value FROM json_each(${JSON.stringify(tags)})
|
|
43
|
+
`;
|
|
44
|
+
assert.ok(insertResult.changes >= 0, 'Invalid tag insert result');
|
|
50
45
|
};
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return id;
|
|
46
|
+
const replaceTags = (memoryId, tags) => {
|
|
47
|
+
const deleteResult = sqlRun `DELETE FROM tags WHERE memory_id = ${memoryId}`;
|
|
48
|
+
assert.ok(deleteResult.changes >= 0, 'Invalid tag delete result');
|
|
49
|
+
insertTags(memoryId, normalizeTags(tags, MAX_TAGS));
|
|
56
50
|
};
|
|
51
|
+
const buildHash = (content) => crypto.createHash('sha256').update(content).digest('hex');
|
|
57
52
|
const resolveMemoryId = (content, hash, importance, memoryType) => {
|
|
58
|
-
const
|
|
59
|
-
|
|
53
|
+
const inserted = sqlGet `
|
|
54
|
+
INSERT OR IGNORE INTO memories (content, hash, importance, memory_type)
|
|
55
|
+
VALUES (${content}, ${hash}, ${importance}, ${memoryType})
|
|
56
|
+
RETURNING id
|
|
57
|
+
`;
|
|
60
58
|
if (inserted) {
|
|
61
59
|
return { id: toSafeInteger(inserted.id, 'id'), isNew: true };
|
|
62
60
|
}
|
|
63
|
-
const
|
|
64
|
-
|
|
61
|
+
const existingId = findMemoryIdByHash(hash);
|
|
62
|
+
assert.ok(existingId !== undefined, 'Failed to resolve memory id');
|
|
63
|
+
return { id: existingId, isNew: false };
|
|
65
64
|
};
|
|
66
|
-
|
|
67
|
-
const createMemoryInTransaction = (input) => {
|
|
65
|
+
const prepareMemory = (input) => {
|
|
68
66
|
const { content, tags = [], importance = 0, memory_type: memoryType = 'general', } = input;
|
|
69
67
|
const hash = buildHash(content);
|
|
70
68
|
const normalizedTags = normalizeTags(tags, MAX_TAGS);
|
|
69
|
+
return { content, hash, normalizedTags, importance, memoryType };
|
|
70
|
+
};
|
|
71
|
+
const insertMemory = (input) => {
|
|
72
|
+
const { content, hash, normalizedTags, importance, memoryType } = input;
|
|
71
73
|
const { id, isNew } = resolveMemoryId(content, hash, importance, memoryType);
|
|
72
74
|
insertTags(id, normalizedTags);
|
|
73
75
|
return { id, hash, isNew };
|
|
74
76
|
};
|
|
75
|
-
export const
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
const withSavepoint = (name, fn) => {
|
|
79
|
-
executeRun(prepareCached(`SAVEPOINT ${name}`));
|
|
77
|
+
export const createMemory = (input) => withImmediateTransaction(() => insertMemory(prepareMemory(input)));
|
|
78
|
+
const createMemoryWithSavepoint = (index, item, signal) => {
|
|
79
|
+
const savepointName = `mem_item_${index}`;
|
|
80
80
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
throwIfAborted(signal);
|
|
82
|
+
const prepared = prepareMemory(item);
|
|
83
|
+
const created = withSavepoint(savepointName, () => insertMemory(prepared));
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
index,
|
|
87
|
+
hash: created.hash,
|
|
88
|
+
isNew: created.isNew,
|
|
89
|
+
};
|
|
84
90
|
}
|
|
85
91
|
catch (err) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
throw err;
|
|
92
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
93
|
+
return { ok: false, index, error: message };
|
|
89
94
|
}
|
|
90
95
|
};
|
|
91
|
-
const createMemoriesInTransaction = (items) => {
|
|
96
|
+
const createMemoriesInTransaction = (items, signal) => {
|
|
92
97
|
const results = [];
|
|
93
98
|
let succeeded = 0;
|
|
94
99
|
let failed = 0;
|
|
100
|
+
throwIfAborted(signal);
|
|
95
101
|
for (let i = 0; i < items.length; i++) {
|
|
102
|
+
throwIfAborted(signal);
|
|
96
103
|
const item = items[i];
|
|
97
104
|
if (!item)
|
|
98
105
|
continue;
|
|
99
|
-
const result = createMemoryWithSavepoint(i, item);
|
|
106
|
+
const result = createMemoryWithSavepoint(i, item, signal);
|
|
100
107
|
results.push(result);
|
|
101
|
-
if (result.ok)
|
|
108
|
+
if (result.ok)
|
|
102
109
|
succeeded++;
|
|
103
|
-
|
|
104
|
-
else {
|
|
110
|
+
else
|
|
105
111
|
failed++;
|
|
106
|
-
}
|
|
107
112
|
}
|
|
108
113
|
return { results, succeeded, failed };
|
|
109
114
|
};
|
|
110
|
-
const
|
|
111
|
-
const savepointName = `mem_item_${index}`;
|
|
112
|
-
try {
|
|
113
|
-
const created = withSavepoint(savepointName, () => createMemoryInTransaction(item));
|
|
114
|
-
return {
|
|
115
|
-
ok: true,
|
|
116
|
-
index,
|
|
117
|
-
hash: created.hash,
|
|
118
|
-
isNew: created.isNew,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
123
|
-
return { ok: false, index, error: message };
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
const replaceTags = (memoryId, tags) => {
|
|
127
|
-
const stmtDeleteTagsForMemory = prepareCached('DELETE FROM tags WHERE memory_id = ?');
|
|
128
|
-
executeRun(stmtDeleteTagsForMemory, memoryId);
|
|
129
|
-
insertTags(memoryId, normalizeTags(tags, MAX_TAGS));
|
|
130
|
-
};
|
|
115
|
+
export const createMemories = (items, signal) => withImmediateTransaction(() => createMemoriesInTransaction(items, signal));
|
|
131
116
|
const assertNoDuplicateOnUpdate = (oldHash, newHash) => {
|
|
132
117
|
if (newHash === oldHash)
|
|
133
118
|
return;
|
|
@@ -136,22 +121,21 @@ const assertNoDuplicateOnUpdate = (oldHash, newHash) => {
|
|
|
136
121
|
throw new Error('Content already exists as another memory');
|
|
137
122
|
}
|
|
138
123
|
};
|
|
139
|
-
|
|
124
|
+
const updateMemoryInTransaction = (hash, options) => {
|
|
140
125
|
const memoryId = findMemoryIdByHash(hash);
|
|
141
126
|
if (memoryId === undefined)
|
|
142
127
|
throw new Error('Memory not found');
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
});
|
|
128
|
+
const newHash = buildHash(options.content);
|
|
129
|
+
assertNoDuplicateOnUpdate(hash, newHash);
|
|
130
|
+
const updateResult = sqlRun `
|
|
131
|
+
UPDATE memories
|
|
132
|
+
SET content = ${options.content}, hash = ${newHash}
|
|
133
|
+
WHERE id = ${memoryId}
|
|
134
|
+
`;
|
|
135
|
+
assert.ok(updateResult.changes >= 0, 'Invalid update result');
|
|
136
|
+
if (options.tags !== undefined) {
|
|
137
|
+
replaceTags(memoryId, options.tags);
|
|
138
|
+
}
|
|
139
|
+
return { updated: true, oldHash: hash, newHash };
|
|
156
140
|
};
|
|
157
|
-
|
|
141
|
+
export const updateMemory = (hash, options) => withImmediateTransaction(() => updateMemoryInTransaction(hash, options));
|
|
@@ -1,78 +1,75 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const
|
|
3
|
-
const stmtFindMemoryIdByHash = prepareCached('SELECT id FROM memories WHERE 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
|
-
};
|
|
1
|
+
import { mapRowToRelationship, requireMemoryIdByHash, sqlAll, sqlGet, sqlRun, toSafeInteger, withImmediateTransaction, } from './db.js';
|
|
2
|
+
const buildNotFoundMessage = (hash) => `Memory not found: ${hash}`;
|
|
16
3
|
export const createRelationship = (input) => withImmediateTransaction(() => {
|
|
17
|
-
const fromId =
|
|
18
|
-
const toId =
|
|
4
|
+
const fromId = requireMemoryIdByHash(input.from_hash, buildNotFoundMessage(input.from_hash));
|
|
5
|
+
const toId = requireMemoryIdByHash(input.to_hash, buildNotFoundMessage(input.to_hash));
|
|
19
6
|
if (fromId === toId) {
|
|
20
7
|
throw new Error('Cannot create self-referential relationship');
|
|
21
8
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
9
|
+
const inserted = sqlGet `
|
|
10
|
+
INSERT OR IGNORE INTO relationships
|
|
11
|
+
(from_memory_id, to_memory_id, relation_type)
|
|
12
|
+
VALUES (${fromId}, ${toId}, ${input.relation_type})
|
|
13
|
+
RETURNING id
|
|
14
|
+
`;
|
|
28
15
|
if (inserted) {
|
|
29
16
|
return { id: toSafeInteger(inserted.id, 'id'), isNew: true };
|
|
30
17
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
const existing = sqlGet `
|
|
19
|
+
SELECT id FROM relationships
|
|
20
|
+
WHERE from_memory_id = ${fromId}
|
|
21
|
+
AND to_memory_id = ${toId}
|
|
22
|
+
AND relation_type = ${input.relation_type}
|
|
23
|
+
`;
|
|
37
24
|
if (!existing) {
|
|
38
25
|
throw new Error('Failed to resolve relationship id');
|
|
39
26
|
}
|
|
40
27
|
return { id: toSafeInteger(existing.id, 'id'), isNew: false };
|
|
41
28
|
});
|
|
42
|
-
// Query that joins with memories to get hashes instead of IDs
|
|
43
|
-
const buildGetRelationshipsQuery = (direction) => {
|
|
44
|
-
const baseSelect = `
|
|
45
|
-
SELECT r.id, r.relation_type, r.created_at,
|
|
46
|
-
mf.hash as from_hash, mt.hash as to_hash
|
|
47
|
-
FROM relationships r
|
|
48
|
-
JOIN memories mf ON r.from_memory_id = mf.id
|
|
49
|
-
JOIN memories mt ON r.to_memory_id = mt.id
|
|
50
|
-
`;
|
|
51
|
-
const orderBy = ' ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id';
|
|
52
|
-
switch (direction) {
|
|
53
|
-
case 'outgoing':
|
|
54
|
-
return `${baseSelect} WHERE mf.hash = ?${orderBy}`;
|
|
55
|
-
case 'incoming':
|
|
56
|
-
return `${baseSelect} WHERE mt.hash = ?${orderBy}`;
|
|
57
|
-
case 'both':
|
|
58
|
-
return `${baseSelect} WHERE mf.hash = ? OR mt.hash = ?${orderBy}`;
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
29
|
export const getRelationships = (input) => {
|
|
62
30
|
const direction = input.direction ?? 'both';
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
31
|
+
const rows = (() => {
|
|
32
|
+
switch (direction) {
|
|
33
|
+
case 'outgoing':
|
|
34
|
+
return sqlAll `
|
|
35
|
+
SELECT r.id, r.relation_type, r.created_at,
|
|
36
|
+
mf.hash as from_hash, mt.hash as to_hash
|
|
37
|
+
FROM relationships r
|
|
38
|
+
JOIN memories mf ON r.from_memory_id = mf.id
|
|
39
|
+
JOIN memories mt ON r.to_memory_id = mt.id
|
|
40
|
+
WHERE mf.hash = ${input.hash}
|
|
41
|
+
ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
|
|
42
|
+
`;
|
|
43
|
+
case 'incoming':
|
|
44
|
+
return sqlAll `
|
|
45
|
+
SELECT r.id, r.relation_type, r.created_at,
|
|
46
|
+
mf.hash as from_hash, mt.hash as to_hash
|
|
47
|
+
FROM relationships r
|
|
48
|
+
JOIN memories mf ON r.from_memory_id = mf.id
|
|
49
|
+
JOIN memories mt ON r.to_memory_id = mt.id
|
|
50
|
+
WHERE mt.hash = ${input.hash}
|
|
51
|
+
ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
|
|
52
|
+
`;
|
|
53
|
+
case 'both':
|
|
54
|
+
return sqlAll `
|
|
55
|
+
SELECT r.id, r.relation_type, r.created_at,
|
|
56
|
+
mf.hash as from_hash, mt.hash as to_hash
|
|
57
|
+
FROM relationships r
|
|
58
|
+
JOIN memories mf ON r.from_memory_id = mf.id
|
|
59
|
+
JOIN memories mt ON r.to_memory_id = mt.id
|
|
60
|
+
WHERE mf.hash = ${input.hash} OR mt.hash = ${input.hash}
|
|
61
|
+
ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
66
65
|
return rows.map(mapRowToRelationship);
|
|
67
66
|
};
|
|
68
67
|
export const deleteRelationship = (input) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const result = executeRun(stmtDeleteRelationship, input.from_hash, input.to_hash, input.relation_type);
|
|
68
|
+
const result = sqlRun `
|
|
69
|
+
DELETE FROM relationships
|
|
70
|
+
WHERE from_memory_id = (SELECT id FROM memories WHERE hash = ${input.from_hash})
|
|
71
|
+
AND to_memory_id = (SELECT id FROM memories WHERE hash = ${input.to_hash})
|
|
72
|
+
AND relation_type = ${input.relation_type}
|
|
73
|
+
`;
|
|
76
74
|
return { changes: toSafeInteger(result.changes, 'changes') };
|
|
77
75
|
};
|
|
78
|
-
//# sourceMappingURL=relationships.js.map
|
package/dist/core/search.d.ts
CHANGED