@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.
Files changed (143) hide show
  1. package/dist/config.d.ts +3 -1
  2. package/dist/config.js +27 -3
  3. package/dist/core/db.d.ts +1 -1
  4. package/dist/core/db.js +11 -0
  5. package/dist/core/memory-read.js +18 -16
  6. package/dist/core/memory-write.d.ts +5 -1
  7. package/dist/core/memory-write.js +33 -22
  8. package/dist/core/relationships.d.ts +15 -0
  9. package/dist/core/relationships.js +93 -0
  10. package/dist/core/search.d.ts +5 -1
  11. package/dist/core/search.js +93 -7
  12. package/dist/index.js +8 -4
  13. package/dist/protocol-version-guard.js +16 -0
  14. package/dist/schemas.d.ts +44 -0
  15. package/dist/schemas.js +58 -0
  16. package/dist/stdio-transport.d.ts +29 -0
  17. package/dist/stdio-transport.js +250 -0
  18. package/dist/tools.d.ts +11 -2
  19. package/dist/tools.js +101 -3
  20. package/dist/types.d.ts +29 -5
  21. package/dist/types.js +10 -1
  22. package/package.json +3 -3
  23. package/dist/core/database-schema.d.ts +0 -2
  24. package/dist/core/database-schema.d.ts.map +0 -1
  25. package/dist/core/database-schema.js +0 -64
  26. package/dist/core/database-schema.js.map +0 -1
  27. package/dist/core/database.d.ts +0 -3
  28. package/dist/core/database.d.ts.map +0 -1
  29. package/dist/core/database.js +0 -43
  30. package/dist/core/database.js.map +0 -1
  31. package/dist/core/memory-create.d.ts +0 -7
  32. package/dist/core/memory-create.d.ts.map +0 -1
  33. package/dist/core/memory-create.js +0 -40
  34. package/dist/core/memory-create.js.map +0 -1
  35. package/dist/core/memory-db.d.ts +0 -2
  36. package/dist/core/memory-db.d.ts.map +0 -1
  37. package/dist/core/memory-db.js +0 -31
  38. package/dist/core/memory-db.js.map +0 -1
  39. package/dist/core/memory-read.d.ts.map +0 -1
  40. package/dist/core/memory-read.js.map +0 -1
  41. package/dist/core/memory-relations.d.ts +0 -10
  42. package/dist/core/memory-relations.d.ts.map +0 -1
  43. package/dist/core/memory-relations.js +0 -57
  44. package/dist/core/memory-relations.js.map +0 -1
  45. package/dist/core/memory-search.d.ts +0 -10
  46. package/dist/core/memory-search.d.ts.map +0 -1
  47. package/dist/core/memory-search.js +0 -23
  48. package/dist/core/memory-search.js.map +0 -1
  49. package/dist/core/memory-stats.d.ts +0 -2
  50. package/dist/core/memory-stats.d.ts.map +0 -1
  51. package/dist/core/memory-stats.js +0 -51
  52. package/dist/core/memory-stats.js.map +0 -1
  53. package/dist/core/memory-updates.d.ts +0 -10
  54. package/dist/core/memory-updates.d.ts.map +0 -1
  55. package/dist/core/memory-updates.js +0 -115
  56. package/dist/core/memory-updates.js.map +0 -1
  57. package/dist/core/relation-queries.d.ts +0 -7
  58. package/dist/core/relation-queries.d.ts.map +0 -1
  59. package/dist/core/relation-queries.js +0 -125
  60. package/dist/core/relation-queries.js.map +0 -1
  61. package/dist/core/relations.d.ts +0 -10
  62. package/dist/core/relations.js +0 -177
  63. package/dist/core/row-mappers.d.ts +0 -6
  64. package/dist/core/row-mappers.d.ts.map +0 -1
  65. package/dist/core/row-mappers.js +0 -52
  66. package/dist/core/row-mappers.js.map +0 -1
  67. package/dist/core/search-errors.d.ts +0 -1
  68. package/dist/core/search-errors.d.ts.map +0 -1
  69. package/dist/core/search-errors.js +0 -30
  70. package/dist/core/search-errors.js.map +0 -1
  71. package/dist/core/search.d.ts.map +0 -1
  72. package/dist/core/search.js.map +0 -1
  73. package/dist/core/sqlite.d.ts +0 -10
  74. package/dist/core/sqlite.d.ts.map +0 -1
  75. package/dist/core/sqlite.js +0 -71
  76. package/dist/core/sqlite.js.map +0 -1
  77. package/dist/core/tags.d.ts +0 -1
  78. package/dist/core/tags.d.ts.map +0 -1
  79. package/dist/core/tags.js +0 -27
  80. package/dist/core/tags.js.map +0 -1
  81. package/dist/index.d.ts.map +0 -1
  82. package/dist/index.js.map +0 -1
  83. package/dist/lib/errors.d.ts +0 -19
  84. package/dist/lib/errors.d.ts.map +0 -1
  85. package/dist/lib/errors.js +0 -22
  86. package/dist/lib/errors.js.map +0 -1
  87. package/dist/schemas/inputs.d.ts +0 -44
  88. package/dist/schemas/inputs.d.ts.map +0 -1
  89. package/dist/schemas/inputs.js +0 -97
  90. package/dist/schemas/inputs.js.map +0 -1
  91. package/dist/schemas/outputs.d.ts +0 -9
  92. package/dist/schemas/outputs.d.ts.map +0 -1
  93. package/dist/schemas/outputs.js +0 -28
  94. package/dist/schemas/outputs.js.map +0 -1
  95. package/dist/tools/definitions/memory-core.d.ts +0 -2
  96. package/dist/tools/definitions/memory-core.d.ts.map +0 -1
  97. package/dist/tools/definitions/memory-core.js +0 -79
  98. package/dist/tools/definitions/memory-core.js.map +0 -1
  99. package/dist/tools/definitions/memory-relations.d.ts +0 -2
  100. package/dist/tools/definitions/memory-relations.d.ts.map +0 -1
  101. package/dist/tools/definitions/memory-relations.js +0 -43
  102. package/dist/tools/definitions/memory-relations.js.map +0 -1
  103. package/dist/tools/definitions/memory-search.d.ts +0 -2
  104. package/dist/tools/definitions/memory-search.d.ts.map +0 -1
  105. package/dist/tools/definitions/memory-search.js +0 -20
  106. package/dist/tools/definitions/memory-search.js.map +0 -1
  107. package/dist/tools/definitions/memory-stats.d.ts +0 -2
  108. package/dist/tools/definitions/memory-stats.d.ts.map +0 -1
  109. package/dist/tools/definitions/memory-stats.js +0 -20
  110. package/dist/tools/definitions/memory-stats.js.map +0 -1
  111. package/dist/tools/index.d.ts +0 -2
  112. package/dist/tools/index.d.ts.map +0 -1
  113. package/dist/tools/index.js +0 -15
  114. package/dist/tools/index.js.map +0 -1
  115. package/dist/tools/tool-handlers.d.ts +0 -3
  116. package/dist/tools/tool-handlers.d.ts.map +0 -1
  117. package/dist/tools/tool-handlers.js +0 -19
  118. package/dist/tools/tool-handlers.js.map +0 -1
  119. package/dist/tools/tool-types.d.ts +0 -14
  120. package/dist/tools/tool-types.d.ts.map +0 -1
  121. package/dist/tools/tool-types.js +0 -1
  122. package/dist/tools/tool-types.js.map +0 -1
  123. package/dist/tsconfig.tsbuildinfo +0 -1
  124. package/dist/types/index.d.ts +0 -37
  125. package/dist/types/index.d.ts.map +0 -1
  126. package/dist/types/index.js +0 -1
  127. package/dist/types/index.js.map +0 -1
  128. package/dist/utils/config.d.ts +0 -6
  129. package/dist/utils/config.d.ts.map +0 -1
  130. package/dist/utils/config.js +0 -99
  131. package/dist/utils/config.js.map +0 -1
  132. package/dist/utils/logger.d.ts +0 -5
  133. package/dist/utils/logger.d.ts.map +0 -1
  134. package/dist/utils/logger.js +0 -20
  135. package/dist/utils/logger.js.map +0 -1
  136. package/dist/utils.d.ts +0 -11
  137. package/dist/utils.js +0 -118
  138. package/dist/worker/db-worker-client.d.ts +0 -9
  139. package/dist/worker/db-worker-client.js +0 -93
  140. package/dist/worker/db-worker.d.ts +0 -1
  141. package/dist/worker/db-worker.js +0 -174
  142. package/dist/worker/protocol.d.ts +0 -9
  143. package/dist/worker/protocol.js +0 -14
package/dist/config.d.ts CHANGED
@@ -1,4 +1,6 @@
1
+ type LogLevel = 'error' | 'info' | 'warn';
1
2
  export declare const config: {
2
3
  dbPath: string;
3
- logLevel: "info";
4
+ logLevel: LogLevel;
4
5
  };
6
+ export {};
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
- if (env.MEMDB_PATH === ':memory:')
7
- return ':memory:';
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 { Memory, SearchResult } from '../types.js';
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'),
@@ -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
- for (const hash of hashes) {
17
- try {
18
- const result = deleteMemory(hash);
19
- if (result.changes > 0) {
20
- results.push({ hash, deleted: true });
21
- succeeded++;
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
- else {
24
- results.push({ hash, deleted: false, error: 'Memory not found' });
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
- catch (err) {
29
- const message = err instanceof Error ? err.message : 'Unknown error';
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
- const { content, tags = [] } = input;
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
- for (let i = 0; i < items.length; i++) {
91
- const item = items[i];
92
- if (!item)
93
- continue;
94
- try {
95
- const { hash, isNew } = createMemory(item);
96
- results.push({ index: i, hash, isNew });
97
- succeeded++;
98
- }
99
- catch (err) {
100
- const message = err instanceof Error ? err.message : 'Unknown error';
101
- results.push({ index: i, error: message });
102
- failed++;
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
- return { results, succeeded, failed };
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
+ };
@@ -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 {};
@@ -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 relevance
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 relevance
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 * FROM content_matches
47
+ SELECT *, (base_relevance + recency_bonus) as relevance FROM content_matches
46
48
  UNION ALL
47
- SELECT * FROM tag_matches
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 patched = target;
29
- patched.createToolError = (message) => {
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 StdioServerTransport();
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>;