mcp-memory-keeper 0.10.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1051 -0
  4. package/bin/mcp-memory-keeper +52 -0
  5. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  6. package/dist/__tests__/helpers/test-server.js +92 -0
  7. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  8. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  9. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  10. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  11. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  12. package/dist/__tests__/integration/channels.test.js +376 -0
  13. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  14. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  15. package/dist/__tests__/integration/context-operations.test.js +243 -0
  16. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  17. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  18. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  19. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  20. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  21. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  22. package/dist/__tests__/integration/contextSearch.test.js +938 -0
  23. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  24. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  25. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  26. package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
  27. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  28. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  29. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  30. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  31. package/dist/__tests__/integration/error-cases.test.js +407 -0
  32. package/dist/__tests__/integration/export-import.test.js +367 -0
  33. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  34. package/dist/__tests__/integration/file-operations.test.js +264 -0
  35. package/dist/__tests__/integration/git-integration.test.js +237 -0
  36. package/dist/__tests__/integration/index-tools.test.js +496 -0
  37. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  38. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  39. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  40. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  41. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  42. package/dist/__tests__/integration/migrations.test.js +528 -0
  43. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  44. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  45. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  46. package/dist/__tests__/integration/project-directory.test.js +283 -0
  47. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  48. package/dist/__tests__/integration/retention.test.js +513 -0
  49. package/dist/__tests__/integration/search.test.js +333 -0
  50. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  51. package/dist/__tests__/integration/server-initialization.test.js +307 -0
  52. package/dist/__tests__/integration/session-management.test.js +219 -0
  53. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  54. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  55. package/dist/__tests__/integration/summarization.test.js +308 -0
  56. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  57. package/dist/__tests__/security/input-validation.test.js +115 -0
  58. package/dist/__tests__/utils/agents.test.js +473 -0
  59. package/dist/__tests__/utils/database.test.js +177 -0
  60. package/dist/__tests__/utils/git.test.js +122 -0
  61. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  62. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  63. package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
  64. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  65. package/dist/__tests__/utils/validation.test.js +200 -0
  66. package/dist/__tests__/utils/vector-store.test.js +231 -0
  67. package/dist/handlers/contextWatchHandlers.js +206 -0
  68. package/dist/index.js +4310 -0
  69. package/dist/index.phase1.backup.js +410 -0
  70. package/dist/index.phase2.backup.js +704 -0
  71. package/dist/migrations/003_add_channels.js +174 -0
  72. package/dist/migrations/004_add_context_watch.js +151 -0
  73. package/dist/migrations/005_add_context_watch.js +98 -0
  74. package/dist/migrations/simplify-sharing.js +117 -0
  75. package/dist/repositories/BaseRepository.js +30 -0
  76. package/dist/repositories/CheckpointRepository.js +140 -0
  77. package/dist/repositories/ContextRepository.js +1873 -0
  78. package/dist/repositories/FileRepository.js +104 -0
  79. package/dist/repositories/RepositoryManager.js +62 -0
  80. package/dist/repositories/SessionRepository.js +66 -0
  81. package/dist/repositories/WatcherRepository.js +252 -0
  82. package/dist/repositories/index.js +15 -0
  83. package/dist/server.js +384 -0
  84. package/dist/test-helpers/database-helper.js +128 -0
  85. package/dist/types/entities.js +3 -0
  86. package/dist/utils/agents.js +791 -0
  87. package/dist/utils/channels.js +150 -0
  88. package/dist/utils/database.js +731 -0
  89. package/dist/utils/feature-flags.js +476 -0
  90. package/dist/utils/git.js +145 -0
  91. package/dist/utils/knowledge-graph.js +264 -0
  92. package/dist/utils/migrationHealthCheck.js +373 -0
  93. package/dist/utils/migrations.js +452 -0
  94. package/dist/utils/retention.js +460 -0
  95. package/dist/utils/timestamps.js +112 -0
  96. package/dist/utils/validation.js +296 -0
  97. package/dist/utils/vector-store.js +247 -0
  98. package/package.json +84 -0
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ValidationError = void 0;
37
+ exports.validateFilePath = validateFilePath;
38
+ exports.validateSearchQuery = validateSearchQuery;
39
+ exports.validateSessionName = validateSessionName;
40
+ exports.validateKey = validateKey;
41
+ exports.validateValue = validateValue;
42
+ exports.validateCategory = validateCategory;
43
+ exports.validatePriority = validatePriority;
44
+ const path = __importStar(require("path"));
45
+ const fs = __importStar(require("fs"));
46
+ class ValidationError extends Error {
47
+ constructor(message) {
48
+ super(message);
49
+ this.name = 'ValidationError';
50
+ }
51
+ }
52
+ exports.ValidationError = ValidationError;
53
+ function validateFilePath(filePath, mode) {
54
+ if (!filePath || typeof filePath !== 'string') {
55
+ throw new ValidationError('File path must be a non-empty string');
56
+ }
57
+ // Check for null bytes
58
+ if (filePath.includes('\0')) {
59
+ throw new ValidationError('File path contains invalid characters');
60
+ }
61
+ // Check for common Windows reserved names
62
+ const basename = path.basename(filePath).toLowerCase();
63
+ const reservedNames = [
64
+ 'con',
65
+ 'prn',
66
+ 'aux',
67
+ 'nul',
68
+ 'com1',
69
+ 'com2',
70
+ 'com3',
71
+ 'com4',
72
+ 'com5',
73
+ 'com6',
74
+ 'com7',
75
+ 'com8',
76
+ 'com9',
77
+ 'lpt1',
78
+ 'lpt2',
79
+ 'lpt3',
80
+ 'lpt4',
81
+ 'lpt5',
82
+ 'lpt6',
83
+ 'lpt7',
84
+ 'lpt8',
85
+ 'lpt9',
86
+ ];
87
+ if (reservedNames.includes(basename.split('.')[0])) {
88
+ throw new ValidationError('File path contains reserved name');
89
+ }
90
+ // Normalize the path
91
+ const normalizedPath = path.normalize(filePath);
92
+ // Check for path traversal attempts
93
+ if (normalizedPath.includes('..') || filePath.includes('../') || filePath.includes('..\\')) {
94
+ throw new ValidationError('Path traversal detected');
95
+ }
96
+ // Block access to system directories
97
+ const blockedPaths = ['/etc/', '/sys/', '/proc/', '\\Windows\\', '\\System32\\'];
98
+ const lowerPath = normalizedPath.toLowerCase();
99
+ for (const blocked of blockedPaths) {
100
+ if (lowerPath.includes(blocked.toLowerCase())) {
101
+ throw new ValidationError('Access to system directories not allowed');
102
+ }
103
+ }
104
+ // Block absolute paths to sensitive files
105
+ if (normalizedPath.startsWith('/etc/passwd') || normalizedPath.includes('\\config\\sam')) {
106
+ throw new ValidationError('Access to sensitive files not allowed');
107
+ }
108
+ if (mode === 'read') {
109
+ // Check if file exists for read operations
110
+ if (!fs.existsSync(normalizedPath)) {
111
+ throw new ValidationError(`File not found: ${normalizedPath}`);
112
+ }
113
+ }
114
+ else {
115
+ // Check if directory exists for write operations
116
+ const dir = path.dirname(normalizedPath);
117
+ if (!fs.existsSync(dir)) {
118
+ throw new ValidationError(`Directory not found: ${dir}`);
119
+ }
120
+ }
121
+ return normalizedPath;
122
+ }
123
+ function validateSearchQuery(query) {
124
+ if (!query || typeof query !== 'string') {
125
+ throw new ValidationError('Search query must be a non-empty string');
126
+ }
127
+ // Remove potentially dangerous SQL characters
128
+ let sanitized = query
129
+ .replace(/['"`;\\]/g, '') // Remove quotes, semicolons, backslashes
130
+ .replace(/--/g, '') // Remove SQL comments
131
+ .replace(/\/\*/g, '') // Remove block comment starts
132
+ .replace(/\*\//g, '') // Remove block comment ends
133
+ .replace(/[%_]/g, '\\$&') // Escape wildcards
134
+ .trim();
135
+ if (sanitized.length === 0) {
136
+ throw new ValidationError('Search query cannot be empty');
137
+ }
138
+ if (sanitized.length > 1000) {
139
+ throw new ValidationError('Search query too long (max 1000 characters)');
140
+ }
141
+ return sanitized;
142
+ }
143
+ function validateSessionName(name) {
144
+ if (!name || typeof name !== 'string') {
145
+ throw new ValidationError('Session name must be a non-empty string');
146
+ }
147
+ const trimmed = name.trim();
148
+ if (trimmed.length === 0) {
149
+ throw new ValidationError('Session name cannot be empty');
150
+ }
151
+ if (trimmed.length > 255) {
152
+ throw new ValidationError('Session name too long (max 255 characters)');
153
+ }
154
+ // Check for path traversal attempts
155
+ if (trimmed.includes('../') || trimmed.includes('..\\')) {
156
+ throw new ValidationError('Session name contains invalid characters');
157
+ }
158
+ // Check for null bytes
159
+ if (trimmed.includes('\0')) {
160
+ throw new ValidationError('Session name contains invalid characters');
161
+ }
162
+ // Check for script injection
163
+ if (/<script|<\/script|javascript:|<iframe|<object|<embed/i.test(trimmed)) {
164
+ throw new ValidationError('Session name contains invalid characters');
165
+ }
166
+ return trimmed;
167
+ }
168
+ function validateKey(key) {
169
+ // Type validation
170
+ if (key === null || key === undefined) {
171
+ throw new ValidationError('Key cannot be null or undefined');
172
+ }
173
+ if (typeof key !== 'string') {
174
+ throw new ValidationError('Key must be a string');
175
+ }
176
+ // Empty string check (before trimming)
177
+ if (key === '') {
178
+ throw new ValidationError('Key cannot be empty');
179
+ }
180
+ // Check if key becomes empty after trimming
181
+ const trimmed = key.trim();
182
+ if (trimmed.length === 0) {
183
+ throw new ValidationError('Key cannot be empty or contain only whitespace');
184
+ }
185
+ // Length validation
186
+ if (trimmed.length > 255) {
187
+ throw new ValidationError('Key too long (max 255 characters)');
188
+ }
189
+ // Check for invalid characters
190
+ // First check for spaces specifically (for better error messages)
191
+ if (/\s/.test(key)) {
192
+ if (/ /.test(key)) {
193
+ throw new ValidationError('Key contains special characters - spaces are not allowed');
194
+ }
195
+ else if (/\t/.test(key)) {
196
+ throw new ValidationError('Key contains special characters - tabs are not allowed');
197
+ }
198
+ else if (/[\n\r]/.test(key)) {
199
+ throw new ValidationError('Key contains special characters (newlines)');
200
+ }
201
+ else {
202
+ throw new ValidationError('Key contains special characters (whitespace)');
203
+ }
204
+ }
205
+ // Check for null bytes
206
+ if (/\0/.test(key)) {
207
+ throw new ValidationError('Key contains invalid characters (null bytes)');
208
+ }
209
+ // Check for control characters (excluding those already checked)
210
+ // eslint-disable-next-line no-control-regex
211
+ if (/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(key)) {
212
+ throw new ValidationError('Key contains control characters');
213
+ }
214
+ // Check for backslashes
215
+ if (/\\/.test(key)) {
216
+ throw new ValidationError('Key contains special characters (backslashes)');
217
+ }
218
+ // Check for quotes
219
+ if (/['"`]/.test(key)) {
220
+ throw new ValidationError('Key contains quotes');
221
+ }
222
+ // Check for shell special characters (including ~)
223
+ if (/[;|&$<>(){}[\]!#~]/.test(key)) {
224
+ throw new ValidationError('Key contains special characters');
225
+ }
226
+ // Check for wildcards
227
+ if (/[*?]/.test(key)) {
228
+ throw new ValidationError('Key contains wildcards (* or ?)');
229
+ }
230
+ // Only allow basic ASCII characters plus underscore, hyphen, dot, forward slash, and colon
231
+ // This will reject all Unicode including emojis, Chinese characters, etc.
232
+ if (!/^[a-zA-Z0-9_\-./:]+$/.test(key)) {
233
+ throw new ValidationError('Key contains special characters');
234
+ }
235
+ // Path traversal protection
236
+ if (key.includes('../') || key.includes('..\\')) {
237
+ throw new ValidationError('Key cannot contain path traversal sequences');
238
+ }
239
+ // SQL injection protection - check for common SQL keywords in suspicious patterns
240
+ const sqlPatterns = [
241
+ /;\s*(DROP|DELETE|INSERT|UPDATE|SELECT|CREATE|ALTER|TRUNCATE)/i,
242
+ /--\s*$/,
243
+ /\/\*.*\*\//,
244
+ /\bUNION\s+SELECT\b/i,
245
+ /\bOR\s+1\s*=\s*1\b/i,
246
+ ];
247
+ for (const pattern of sqlPatterns) {
248
+ if (pattern.test(key)) {
249
+ throw new ValidationError('Key contains potentially malicious SQL patterns');
250
+ }
251
+ }
252
+ // Script injection protection
253
+ if (/<script|<\/script|javascript:|<iframe|<object|<embed|<img.*on\w+=/i.test(key)) {
254
+ throw new ValidationError('Key contains potentially malicious script patterns');
255
+ }
256
+ // If we get here, the key is valid
257
+ return trimmed;
258
+ }
259
+ function validateValue(value) {
260
+ if (typeof value !== 'string') {
261
+ throw new ValidationError('Value must be a string');
262
+ }
263
+ // Allow empty values but check size
264
+ if (value.length > 1000000) {
265
+ // 1MB limit
266
+ throw new ValidationError('Value too large (max 1MB)');
267
+ }
268
+ return value;
269
+ }
270
+ function validateCategory(category) {
271
+ if (!category)
272
+ return undefined;
273
+ const validCategories = [
274
+ 'task',
275
+ 'decision',
276
+ 'progress',
277
+ 'note',
278
+ 'error',
279
+ 'warning',
280
+ 'git',
281
+ 'system',
282
+ ];
283
+ if (!validCategories.includes(category)) {
284
+ throw new ValidationError(`Invalid category. Must be one of: ${validCategories.join(', ')}`);
285
+ }
286
+ return category;
287
+ }
288
+ function validatePriority(priority) {
289
+ if (!priority)
290
+ return 'normal';
291
+ const validPriorities = ['high', 'normal', 'low'];
292
+ if (!validPriorities.includes(priority)) {
293
+ throw new ValidationError(`Invalid priority. Must be one of: ${validPriorities.join(', ')}`);
294
+ }
295
+ return priority;
296
+ }
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.VectorStore = void 0;
37
+ const uuid_1 = require("uuid");
38
+ const crypto = __importStar(require("crypto"));
39
+ class VectorStore {
40
+ db;
41
+ dimension = 384; // Using smaller embeddings for efficiency
42
+ constructor(db) {
43
+ this.db = db;
44
+ this.initializeTables();
45
+ }
46
+ initializeTables() {
47
+ // Create vector storage table
48
+ this.db.exec(`
49
+ CREATE TABLE IF NOT EXISTS vector_embeddings (
50
+ id TEXT PRIMARY KEY,
51
+ content_id TEXT NOT NULL,
52
+ content TEXT NOT NULL,
53
+ embedding BLOB NOT NULL,
54
+ metadata TEXT,
55
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
56
+ FOREIGN KEY (content_id) REFERENCES context_items(id) ON DELETE CASCADE
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_vector_content_id ON vector_embeddings(content_id);
60
+ `);
61
+ }
62
+ // Simple text embedding using character n-grams and hashing
63
+ // This is a lightweight alternative to neural embeddings
64
+ createEmbedding(text) {
65
+ const embedding = new Array(this.dimension).fill(0);
66
+ const normalizedText = text.toLowerCase().replace(/\s+/g, ' ').trim();
67
+ // Generate character trigrams
68
+ const ngrams = [];
69
+ for (let i = 0; i <= normalizedText.length - 3; i++) {
70
+ ngrams.push(normalizedText.slice(i, i + 3));
71
+ }
72
+ // Also add word-level features
73
+ const words = normalizedText.split(' ');
74
+ for (const word of words) {
75
+ if (word.length > 2) {
76
+ ngrams.push(word);
77
+ }
78
+ }
79
+ // Hash each n-gram to a position in the embedding
80
+ for (const ngram of ngrams) {
81
+ const hash = crypto.createHash('md5').update(ngram).digest();
82
+ // Use multiple hash values to set multiple positions
83
+ for (let i = 0; i < 3; i++) {
84
+ const position = ((hash[i * 2] << 8) | hash[i * 2 + 1]) % this.dimension;
85
+ const value = (hash[i * 2 + 2] % 256) / 255.0;
86
+ embedding[position] += value;
87
+ }
88
+ }
89
+ // Normalize the embedding
90
+ const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
91
+ if (magnitude > 0) {
92
+ for (let i = 0; i < embedding.length; i++) {
93
+ embedding[i] /= magnitude;
94
+ }
95
+ }
96
+ return embedding;
97
+ }
98
+ // Cosine similarity between two embeddings
99
+ cosineSimilarity(a, b) {
100
+ let dotProduct = 0;
101
+ let normA = 0;
102
+ let normB = 0;
103
+ for (let i = 0; i < a.length; i++) {
104
+ dotProduct += a[i] * b[i];
105
+ normA += a[i] * a[i];
106
+ normB += b[i] * b[i];
107
+ }
108
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
109
+ return magnitude > 0 ? dotProduct / magnitude : 0;
110
+ }
111
+ // Store a document with its embedding
112
+ async storeDocument(contentId, content, metadata) {
113
+ const id = (0, uuid_1.v4)();
114
+ const embedding = this.createEmbedding(content);
115
+ // Convert embedding to buffer for storage
116
+ const buffer = Buffer.from(new Float32Array(embedding).buffer);
117
+ const stmt = this.db.prepare(`
118
+ INSERT INTO vector_embeddings (id, content_id, content, embedding, metadata)
119
+ VALUES (?, ?, ?, ?, ?)
120
+ `);
121
+ stmt.run(id, contentId, content, buffer, metadata ? JSON.stringify(metadata) : null);
122
+ return id;
123
+ }
124
+ // Search for similar documents
125
+ async search(query, topK = 10, minSimilarity = 0.3) {
126
+ const queryEmbedding = this.createEmbedding(query);
127
+ // Get all embeddings (in production, we'd want to optimize this)
128
+ const rows = this.db
129
+ .prepare('SELECT id, content, embedding, metadata FROM vector_embeddings')
130
+ .all();
131
+ const results = [];
132
+ for (const row of rows) {
133
+ // Convert buffer back to array
134
+ const buffer = row.embedding;
135
+ const embedding = Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
136
+ const similarity = this.cosineSimilarity(queryEmbedding, embedding);
137
+ if (similarity >= minSimilarity) {
138
+ results.push({
139
+ id: row.id,
140
+ content: row.content,
141
+ similarity,
142
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
143
+ });
144
+ }
145
+ }
146
+ // Sort by similarity descending
147
+ results.sort((a, b) => b.similarity - a.similarity);
148
+ return results.slice(0, topK);
149
+ }
150
+ // Search within a specific session
151
+ async searchInSession(sessionId, query, topK = 10, minSimilarity = 0.3) {
152
+ const queryEmbedding = this.createEmbedding(query);
153
+ // Get embeddings for this session
154
+ const rows = this.db
155
+ .prepare(`
156
+ SELECT ve.id, ve.content, ve.embedding, ve.metadata
157
+ FROM vector_embeddings ve
158
+ JOIN context_items ci ON ve.content_id = ci.id
159
+ WHERE ci.session_id = ?
160
+ `)
161
+ .all(sessionId);
162
+ const results = [];
163
+ for (const row of rows) {
164
+ const buffer = row.embedding;
165
+ const embedding = Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
166
+ const similarity = this.cosineSimilarity(queryEmbedding, embedding);
167
+ if (similarity >= minSimilarity) {
168
+ results.push({
169
+ id: row.id,
170
+ content: row.content,
171
+ similarity,
172
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
173
+ });
174
+ }
175
+ }
176
+ results.sort((a, b) => b.similarity - a.similarity);
177
+ return results.slice(0, topK);
178
+ }
179
+ // Find related documents to a given document
180
+ async findRelated(documentId, topK = 10, minSimilarity = 0.3) {
181
+ // Get the document's embedding
182
+ const doc = this.db
183
+ .prepare('SELECT content, embedding FROM vector_embeddings WHERE id = ?')
184
+ .get(documentId);
185
+ if (!doc) {
186
+ return [];
187
+ }
188
+ const buffer = doc.embedding;
189
+ const targetEmbedding = Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
190
+ // Get all other embeddings
191
+ const rows = this.db
192
+ .prepare('SELECT id, content, embedding, metadata FROM vector_embeddings WHERE id != ?')
193
+ .all(documentId);
194
+ const results = [];
195
+ for (const row of rows) {
196
+ const rowBuffer = row.embedding;
197
+ const embedding = Array.from(new Float32Array(rowBuffer.buffer, rowBuffer.byteOffset, rowBuffer.byteLength / 4));
198
+ const similarity = this.cosineSimilarity(targetEmbedding, embedding);
199
+ if (similarity >= minSimilarity) {
200
+ results.push({
201
+ id: row.id,
202
+ content: row.content,
203
+ similarity,
204
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
205
+ });
206
+ }
207
+ }
208
+ results.sort((a, b) => b.similarity - a.similarity);
209
+ return results.slice(0, topK);
210
+ }
211
+ // Update embeddings for all context items in a session
212
+ async updateSessionEmbeddings(sessionId) {
213
+ // Get all context items without embeddings
214
+ const items = this.db
215
+ .prepare(`
216
+ SELECT ci.id, ci.key, ci.value, ci.category, ci.priority
217
+ FROM context_items ci
218
+ LEFT JOIN vector_embeddings ve ON ci.id = ve.content_id
219
+ WHERE ci.session_id = ? AND ve.id IS NULL
220
+ `)
221
+ .all(sessionId);
222
+ let count = 0;
223
+ for (const item of items) {
224
+ const content = `${item.key}: ${item.value}`;
225
+ const metadata = {
226
+ key: item.key,
227
+ category: item.category,
228
+ priority: item.priority,
229
+ };
230
+ await this.storeDocument(item.id, content, metadata);
231
+ count++;
232
+ }
233
+ return count;
234
+ }
235
+ // Delete embeddings for a content item
236
+ deleteEmbedding(contentId) {
237
+ this.db.prepare('DELETE FROM vector_embeddings WHERE content_id = ?').run(contentId);
238
+ }
239
+ // Get statistics
240
+ getStats() {
241
+ const count = this.db.prepare('SELECT COUNT(*) as count FROM vector_embeddings').get();
242
+ return {
243
+ totalDocuments: count.count,
244
+ };
245
+ }
246
+ }
247
+ exports.VectorStore = VectorStore;
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "mcp-memory-keeper",
3
+ "version": "0.10.0",
4
+ "description": "MCP server for persistent context management in AI coding assistants",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "mcp-memory-keeper": "./bin/mcp-memory-keeper"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/index.ts",
12
+ "start": "node dist/index.js",
13
+ "test": "jest",
14
+ "test:watch": "jest --watch",
15
+ "test:coverage": "jest --coverage",
16
+ "lint": "eslint . --ext .ts",
17
+ "lint:fix": "eslint . --ext .ts --fix",
18
+ "lint:warn": "eslint . --ext .ts --max-warnings=-1 || true",
19
+ "format": "prettier --write \"src/**/*.ts\"",
20
+ "format:check": "prettier --check \"src/**/*.ts\"",
21
+ "type-check": "tsc --noEmit",
22
+ "check-all": "npm run build && npm run type-check && npm run lint && npm run format:check && npm test",
23
+ "pre-commit": "npm run build && npm run type-check && npm run lint && npm run format:check && npm test",
24
+ "check-migrations": "tsx scripts/check-migrations.ts",
25
+ "prepare": "husky",
26
+ "prepublishOnly": "npm run build && npm run test"
27
+ },
28
+ "keywords": [
29
+ "mcp",
30
+ "claude",
31
+ "ai",
32
+ "context",
33
+ "coding-assistant"
34
+ ],
35
+ "author": "Mark Kreyman",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/mkreyman/mcp-memory-keeper"
40
+ },
41
+ "homepage": "https://github.com/mkreyman/mcp-memory-keeper#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/mkreyman/mcp-memory-keeper/issues"
44
+ },
45
+ "files": [
46
+ "dist/**/*",
47
+ "bin/**/*",
48
+ "README.md",
49
+ "LICENSE",
50
+ "CHANGELOG.md"
51
+ ],
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "dependencies": {
59
+ "@modelcontextprotocol/sdk": "^1.12.3",
60
+ "better-sqlite3": "^11.10.0",
61
+ "simple-git": "^3.28.0",
62
+ "uuid": "^11.1.0"
63
+ },
64
+ "devDependencies": {
65
+ "@eslint/js": "^9.29.0",
66
+ "@jest/globals": "^30.0.0",
67
+ "@types/better-sqlite3": "^7.6.13",
68
+ "@types/jest": "^30.0.0",
69
+ "@types/node": "^24.0.3",
70
+ "@typescript-eslint/eslint-plugin": "^8.34.1",
71
+ "@typescript-eslint/parser": "^8.34.1",
72
+ "eslint": "^9.29.0",
73
+ "eslint-config-prettier": "^10.1.5",
74
+ "eslint-plugin-prettier": "^5.5.0",
75
+ "globals": "^16.2.0",
76
+ "husky": "^9.1.7",
77
+ "jest": "^30.0.0",
78
+ "lint-staged": "^15.5.2",
79
+ "prettier": "^3.5.3",
80
+ "ts-jest": "^29.4.0",
81
+ "tsx": "^4.20.3",
82
+ "typescript": "^5.8.3"
83
+ }
84
+ }