@smallironman/mcp-memory-keeper 0.12.2-fork1
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 +542 -0
- package/LICENSE +21 -0
- package/README.md +1281 -0
- package/bin/mcp-memory-keeper +54 -0
- package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
- package/dist/__tests__/e2e/server-e2e.test.js +341 -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 +1054 -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/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 +411 -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/filterBySessionId.test.js +251 -0
- package/dist/__tests__/integration/git-integration.test.js +241 -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/issue24-final-fix.test.js +241 -0
- package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
- package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
- package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
- package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -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 +291 -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 +305 -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/tokenLimitEnforcement.test.js +134 -0
- package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -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 +192 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/token-limits.test.js +225 -0
- package/dist/__tests__/utils/tool-profiles.test.js +374 -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 +4425 -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 +2017 -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/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 +780 -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/token-limits.js +350 -0
- package/dist/utils/tool-profiles.js +242 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- package/examples/config.json +31 -0
- package/examples/project-directory-setup.md +114 -0
- package/package.json +85 -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;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Place this file at ~/.mcp-memory-keeper/config.json. Built-in profiles (minimal, standard, full) are available without configuration. Define custom profiles or override built-ins here.",
|
|
3
|
+
"profiles": {
|
|
4
|
+
"my_custom_workflow": [
|
|
5
|
+
"context_session_start",
|
|
6
|
+
"context_save",
|
|
7
|
+
"context_get",
|
|
8
|
+
"context_search",
|
|
9
|
+
"context_status",
|
|
10
|
+
"context_checkpoint",
|
|
11
|
+
"context_restore_checkpoint",
|
|
12
|
+
"context_diff",
|
|
13
|
+
"context_timeline",
|
|
14
|
+
"context_batch_save"
|
|
15
|
+
],
|
|
16
|
+
"git_focused": [
|
|
17
|
+
"context_session_start",
|
|
18
|
+
"context_session_list",
|
|
19
|
+
"context_set_project_dir",
|
|
20
|
+
"context_save",
|
|
21
|
+
"context_get",
|
|
22
|
+
"context_search",
|
|
23
|
+
"context_status",
|
|
24
|
+
"context_checkpoint",
|
|
25
|
+
"context_restore_checkpoint",
|
|
26
|
+
"context_git_commit",
|
|
27
|
+
"context_diff",
|
|
28
|
+
"context_export"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Smart Project Directory Management
|
|
2
|
+
|
|
3
|
+
As of version 0.8.3, MCP Memory Keeper provides intelligent assistance for setting up project directories to enable git tracking. This enhancement helps users configure their project directory correctly, especially when starting Claude Code from parent directories.
|
|
4
|
+
|
|
5
|
+
## How it Works
|
|
6
|
+
|
|
7
|
+
When you start a new session without providing a `projectDir`, the server will:
|
|
8
|
+
|
|
9
|
+
1. Check if the current directory has a git repository
|
|
10
|
+
2. Scan subdirectories for git repositories
|
|
11
|
+
3. Provide smart suggestions for setting up the project directory
|
|
12
|
+
4. Prompt for project directory when git operations are attempted
|
|
13
|
+
|
|
14
|
+
## Examples
|
|
15
|
+
|
|
16
|
+
### Starting from Parent Directory
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// If you're in ~/workspace and your project is in ~/workspace/my-project
|
|
20
|
+
await mcp.call('context_session_start', {
|
|
21
|
+
name: 'My Feature Work',
|
|
22
|
+
description: 'Working on the new feature',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Response will show:
|
|
26
|
+
// Started new session: <session-id>
|
|
27
|
+
// Name: My Feature Work
|
|
28
|
+
// Git branch: unknown
|
|
29
|
+
//
|
|
30
|
+
// 💡 Found git repositories in: my-project, another-project
|
|
31
|
+
// To enable git tracking, start a session with your project directory:
|
|
32
|
+
// context_session_start({ name: "My Feature Work", projectDir: "/Users/you/workspace/my-project" })
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Explicit Directory (Override)
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// You can still explicitly set a different directory if needed
|
|
39
|
+
await mcp.call('context_session_start', {
|
|
40
|
+
name: 'Another Project',
|
|
41
|
+
projectDir: '/path/to/different/project',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Response will show:
|
|
45
|
+
// Started new session: <session-id>
|
|
46
|
+
// Name: Another Project
|
|
47
|
+
// Working directory: /path/to/different/project (explicitly set)
|
|
48
|
+
// Git branch: develop
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Non-Git Directory
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// If the directory is not a git repository
|
|
55
|
+
await mcp.call('context_session_start', {
|
|
56
|
+
name: 'Non-Git Project',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Response will show:
|
|
60
|
+
// Started new session: <session-id>
|
|
61
|
+
// Name: Non-Git Project
|
|
62
|
+
// Working directory: /path/to/current/directory (auto-detected)
|
|
63
|
+
// Git: No repository found in working directory
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Git Operations Without Project Directory
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// If you try to commit without setting project directory
|
|
70
|
+
await mcp.call('context_git_commit', {
|
|
71
|
+
message: 'feat: Add new feature',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Response will show:
|
|
75
|
+
// ⚠️ No project directory set for git tracking!
|
|
76
|
+
//
|
|
77
|
+
// To enable git tracking for your project, use one of these methods:
|
|
78
|
+
//
|
|
79
|
+
// 1. For the current session:
|
|
80
|
+
// context_set_project_dir({ projectDir: "/path/to/your/project" })
|
|
81
|
+
//
|
|
82
|
+
// 2. When starting a new session:
|
|
83
|
+
// context_session_start({ name: "My Session", projectDir: "/path/to/your/project" })
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Benefits
|
|
87
|
+
|
|
88
|
+
1. **Smart Suggestions**: Automatically detects git repositories and suggests the right paths
|
|
89
|
+
2. **Clear Guidance**: Provides helpful messages when project directory is needed
|
|
90
|
+
3. **Prevents Mistakes**: Avoids using wrong directories when started from parent folders
|
|
91
|
+
4. **Context Preservation**: The working directory is saved with the session for future reference
|
|
92
|
+
5. **Backward Compatible**: Existing code that provides `projectDir` continues to work as before
|
|
93
|
+
|
|
94
|
+
## Database Schema
|
|
95
|
+
|
|
96
|
+
The sessions table now includes a `working_directory` column to persist this information:
|
|
97
|
+
|
|
98
|
+
```sql
|
|
99
|
+
CREATE TABLE sessions (
|
|
100
|
+
id TEXT PRIMARY KEY,
|
|
101
|
+
name TEXT,
|
|
102
|
+
description TEXT,
|
|
103
|
+
branch TEXT,
|
|
104
|
+
working_directory TEXT, -- New column
|
|
105
|
+
parent_id TEXT,
|
|
106
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
107
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
108
|
+
FOREIGN KEY (parent_id) REFERENCES sessions(id)
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Migration Support
|
|
113
|
+
|
|
114
|
+
For existing databases, the schema is automatically updated when the server starts. The migration checks if the `working_directory` column exists and adds it if missing.
|