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.
- package/CHANGELOG.md +433 -0
- package/LICENSE +21 -0
- package/README.md +1051 -0
- package/bin/mcp-memory-keeper +52 -0
- package/dist/__tests__/helpers/database-test-helper.js +160 -0
- package/dist/__tests__/helpers/test-server.js +92 -0
- package/dist/__tests__/integration/advanced-features.test.js +614 -0
- package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
- package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
- package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
- package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
- package/dist/__tests__/integration/channels.test.js +376 -0
- package/dist/__tests__/integration/checkpoint.test.js +251 -0
- package/dist/__tests__/integration/concurrent-access.test.js +190 -0
- package/dist/__tests__/integration/context-operations.test.js +243 -0
- package/dist/__tests__/integration/contextDiff.test.js +852 -0
- package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
- package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
- package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
- package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
- package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
- package/dist/__tests__/integration/contextSearch.test.js +938 -0
- package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
- package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
- package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
- package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
- package/dist/__tests__/integration/database-initialization.test.js +134 -0
- package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
- package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
- package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
- package/dist/__tests__/integration/error-cases.test.js +407 -0
- package/dist/__tests__/integration/export-import.test.js +367 -0
- package/dist/__tests__/integration/feature-flags.test.js +542 -0
- package/dist/__tests__/integration/file-operations.test.js +264 -0
- package/dist/__tests__/integration/git-integration.test.js +237 -0
- package/dist/__tests__/integration/index-tools.test.js +496 -0
- package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
- package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
- package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
- package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
- package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
- package/dist/__tests__/integration/migrations.test.js +528 -0
- package/dist/__tests__/integration/multi-agent.test.js +546 -0
- package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
- package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
- package/dist/__tests__/integration/project-directory.test.js +283 -0
- package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
- package/dist/__tests__/integration/retention.test.js +513 -0
- package/dist/__tests__/integration/search.test.js +333 -0
- package/dist/__tests__/integration/semantic-search.test.js +266 -0
- package/dist/__tests__/integration/server-initialization.test.js +307 -0
- package/dist/__tests__/integration/session-management.test.js +219 -0
- package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
- package/dist/__tests__/integration/smart-compaction.test.js +230 -0
- package/dist/__tests__/integration/summarization.test.js +308 -0
- package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
- package/dist/__tests__/security/input-validation.test.js +115 -0
- package/dist/__tests__/utils/agents.test.js +473 -0
- package/dist/__tests__/utils/database.test.js +177 -0
- package/dist/__tests__/utils/git.test.js +122 -0
- package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
- package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/validation.test.js +200 -0
- package/dist/__tests__/utils/vector-store.test.js +231 -0
- package/dist/handlers/contextWatchHandlers.js +206 -0
- package/dist/index.js +4310 -0
- package/dist/index.phase1.backup.js +410 -0
- package/dist/index.phase2.backup.js +704 -0
- package/dist/migrations/003_add_channels.js +174 -0
- package/dist/migrations/004_add_context_watch.js +151 -0
- package/dist/migrations/005_add_context_watch.js +98 -0
- package/dist/migrations/simplify-sharing.js +117 -0
- package/dist/repositories/BaseRepository.js +30 -0
- package/dist/repositories/CheckpointRepository.js +140 -0
- package/dist/repositories/ContextRepository.js +1873 -0
- package/dist/repositories/FileRepository.js +104 -0
- package/dist/repositories/RepositoryManager.js +62 -0
- package/dist/repositories/SessionRepository.js +66 -0
- package/dist/repositories/WatcherRepository.js +252 -0
- package/dist/repositories/index.js +15 -0
- package/dist/server.js +384 -0
- package/dist/test-helpers/database-helper.js +128 -0
- package/dist/types/entities.js +3 -0
- package/dist/utils/agents.js +791 -0
- package/dist/utils/channels.js +150 -0
- package/dist/utils/database.js +731 -0
- package/dist/utils/feature-flags.js +476 -0
- package/dist/utils/git.js +145 -0
- package/dist/utils/knowledge-graph.js +264 -0
- package/dist/utils/migrationHealthCheck.js +373 -0
- package/dist/utils/migrations.js +452 -0
- package/dist/utils/retention.js +460 -0
- package/dist/utils/timestamps.js +112 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- 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
|
+
}
|