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,200 @@
|
|
|
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
|
+
const validation_1 = require("../../utils/validation");
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
// Mock fs module
|
|
40
|
+
jest.mock('fs');
|
|
41
|
+
describe('Validation Utils', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
describe('validateFilePath', () => {
|
|
46
|
+
it('should accept valid file paths for write', () => {
|
|
47
|
+
fs.existsSync.mockReturnValue(true);
|
|
48
|
+
const result = (0, validation_1.validateFilePath)('/tmp/test.txt', 'write');
|
|
49
|
+
expect(result).toBe(path.normalize('/tmp/test.txt'));
|
|
50
|
+
});
|
|
51
|
+
it('should accept valid file paths for read', () => {
|
|
52
|
+
fs.existsSync.mockReturnValue(true);
|
|
53
|
+
const result = (0, validation_1.validateFilePath)('/tmp/test.txt', 'read');
|
|
54
|
+
expect(result).toBe(path.normalize('/tmp/test.txt'));
|
|
55
|
+
});
|
|
56
|
+
it('should reject empty file paths', () => {
|
|
57
|
+
expect(() => (0, validation_1.validateFilePath)('', 'read')).toThrow(validation_1.ValidationError);
|
|
58
|
+
expect(() => (0, validation_1.validateFilePath)('', 'read')).toThrow('File path must be a non-empty string');
|
|
59
|
+
});
|
|
60
|
+
it('should reject null/undefined file paths', () => {
|
|
61
|
+
expect(() => (0, validation_1.validateFilePath)(null, 'read')).toThrow(validation_1.ValidationError);
|
|
62
|
+
expect(() => (0, validation_1.validateFilePath)(undefined, 'read')).toThrow(validation_1.ValidationError);
|
|
63
|
+
});
|
|
64
|
+
it('should reject path traversal attempts', () => {
|
|
65
|
+
expect(() => (0, validation_1.validateFilePath)('../../../etc/passwd', 'read')).toThrow(validation_1.ValidationError);
|
|
66
|
+
expect(() => (0, validation_1.validateFilePath)('../../../etc/passwd', 'read')).toThrow('Path traversal detected');
|
|
67
|
+
});
|
|
68
|
+
it('should reject non-existent files for read', () => {
|
|
69
|
+
fs.existsSync.mockReturnValue(false);
|
|
70
|
+
expect(() => (0, validation_1.validateFilePath)('/tmp/nonexistent.txt', 'read')).toThrow(validation_1.ValidationError);
|
|
71
|
+
expect(() => (0, validation_1.validateFilePath)('/tmp/nonexistent.txt', 'read')).toThrow('File not found');
|
|
72
|
+
});
|
|
73
|
+
it('should reject non-existent directories for write', () => {
|
|
74
|
+
fs.existsSync.mockReturnValue(false);
|
|
75
|
+
expect(() => (0, validation_1.validateFilePath)('/nonexistent/dir/file.txt', 'write')).toThrow(validation_1.ValidationError);
|
|
76
|
+
expect(() => (0, validation_1.validateFilePath)('/nonexistent/dir/file.txt', 'write')).toThrow('Directory not found');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('validateSearchQuery', () => {
|
|
80
|
+
it('should accept valid search queries', () => {
|
|
81
|
+
expect((0, validation_1.validateSearchQuery)('test query')).toBe('test query');
|
|
82
|
+
expect((0, validation_1.validateSearchQuery)(' spaces ')).toBe('spaces');
|
|
83
|
+
});
|
|
84
|
+
it('should escape SQL wildcards', () => {
|
|
85
|
+
expect((0, validation_1.validateSearchQuery)('test%query')).toBe('test\\%query');
|
|
86
|
+
expect((0, validation_1.validateSearchQuery)('test_query')).toBe('test\\_query');
|
|
87
|
+
});
|
|
88
|
+
it('should reject empty queries', () => {
|
|
89
|
+
expect(() => (0, validation_1.validateSearchQuery)('')).toThrow(validation_1.ValidationError);
|
|
90
|
+
expect(() => (0, validation_1.validateSearchQuery)(' ')).toThrow('Search query cannot be empty');
|
|
91
|
+
});
|
|
92
|
+
it('should reject null/undefined queries', () => {
|
|
93
|
+
expect(() => (0, validation_1.validateSearchQuery)(null)).toThrow(validation_1.ValidationError);
|
|
94
|
+
expect(() => (0, validation_1.validateSearchQuery)(undefined)).toThrow(validation_1.ValidationError);
|
|
95
|
+
});
|
|
96
|
+
it('should reject queries that are too long', () => {
|
|
97
|
+
const longQuery = 'a'.repeat(1001);
|
|
98
|
+
expect(() => (0, validation_1.validateSearchQuery)(longQuery)).toThrow(validation_1.ValidationError);
|
|
99
|
+
expect(() => (0, validation_1.validateSearchQuery)(longQuery)).toThrow('Search query too long');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('validateSessionName', () => {
|
|
103
|
+
it('should accept valid session names', () => {
|
|
104
|
+
expect((0, validation_1.validateSessionName)('My Session')).toBe('My Session');
|
|
105
|
+
expect((0, validation_1.validateSessionName)(' Trimmed ')).toBe('Trimmed');
|
|
106
|
+
});
|
|
107
|
+
it('should reject empty session names', () => {
|
|
108
|
+
expect(() => (0, validation_1.validateSessionName)('')).toThrow(validation_1.ValidationError);
|
|
109
|
+
expect(() => (0, validation_1.validateSessionName)(' ')).toThrow('Session name cannot be empty');
|
|
110
|
+
});
|
|
111
|
+
it('should reject null/undefined session names', () => {
|
|
112
|
+
expect(() => (0, validation_1.validateSessionName)(null)).toThrow(validation_1.ValidationError);
|
|
113
|
+
expect(() => (0, validation_1.validateSessionName)(undefined)).toThrow(validation_1.ValidationError);
|
|
114
|
+
});
|
|
115
|
+
it('should reject session names that are too long', () => {
|
|
116
|
+
const longName = 'a'.repeat(256);
|
|
117
|
+
expect(() => (0, validation_1.validateSessionName)(longName)).toThrow(validation_1.ValidationError);
|
|
118
|
+
expect(() => (0, validation_1.validateSessionName)(longName)).toThrow('Session name too long');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('validateKey', () => {
|
|
122
|
+
it('should accept valid keys', () => {
|
|
123
|
+
expect((0, validation_1.validateKey)('my_key')).toBe('my_key');
|
|
124
|
+
expect((0, validation_1.validateKey)('valid_key_123')).toBe('valid_key_123');
|
|
125
|
+
expect((0, validation_1.validateKey)('key-with-hyphens')).toBe('key-with-hyphens');
|
|
126
|
+
expect((0, validation_1.validateKey)('key.with.dots')).toBe('key.with.dots');
|
|
127
|
+
expect((0, validation_1.validateKey)('path/to/key')).toBe('path/to/key');
|
|
128
|
+
expect((0, validation_1.validateKey)('namespace:key')).toBe('namespace:key');
|
|
129
|
+
});
|
|
130
|
+
it('should reject keys with leading or trailing whitespace', () => {
|
|
131
|
+
expect(() => (0, validation_1.validateKey)(' trimmed_key ')).toThrow(validation_1.ValidationError);
|
|
132
|
+
expect(() => (0, validation_1.validateKey)(' leading_space')).toThrow(validation_1.ValidationError);
|
|
133
|
+
expect(() => (0, validation_1.validateKey)('trailing_space ')).toThrow(validation_1.ValidationError);
|
|
134
|
+
expect(() => (0, validation_1.validateKey)('\ttab_key')).toThrow(validation_1.ValidationError);
|
|
135
|
+
});
|
|
136
|
+
it('should reject empty keys', () => {
|
|
137
|
+
expect(() => (0, validation_1.validateKey)('')).toThrow(validation_1.ValidationError);
|
|
138
|
+
expect(() => (0, validation_1.validateKey)(' ')).toThrow('Key cannot be empty');
|
|
139
|
+
});
|
|
140
|
+
it('should reject null/undefined keys', () => {
|
|
141
|
+
expect(() => (0, validation_1.validateKey)(null)).toThrow(validation_1.ValidationError);
|
|
142
|
+
expect(() => (0, validation_1.validateKey)(undefined)).toThrow(validation_1.ValidationError);
|
|
143
|
+
});
|
|
144
|
+
it('should reject keys that are too long', () => {
|
|
145
|
+
const longKey = 'k'.repeat(256);
|
|
146
|
+
expect(() => (0, validation_1.validateKey)(longKey)).toThrow(validation_1.ValidationError);
|
|
147
|
+
expect(() => (0, validation_1.validateKey)(longKey)).toThrow('Key too long');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('validateValue', () => {
|
|
151
|
+
it('should accept valid values', () => {
|
|
152
|
+
expect((0, validation_1.validateValue)('test value')).toBe('test value');
|
|
153
|
+
expect((0, validation_1.validateValue)('')).toBe(''); // Empty values are allowed
|
|
154
|
+
});
|
|
155
|
+
it('should reject non-string values', () => {
|
|
156
|
+
expect(() => (0, validation_1.validateValue)(123)).toThrow(validation_1.ValidationError);
|
|
157
|
+
expect(() => (0, validation_1.validateValue)(null)).toThrow('Value must be a string');
|
|
158
|
+
});
|
|
159
|
+
it('should reject values that are too large', () => {
|
|
160
|
+
const largeValue = 'v'.repeat(1000001);
|
|
161
|
+
expect(() => (0, validation_1.validateValue)(largeValue)).toThrow(validation_1.ValidationError);
|
|
162
|
+
expect(() => (0, validation_1.validateValue)(largeValue)).toThrow('Value too large');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('validateCategory', () => {
|
|
166
|
+
it('should accept valid categories', () => {
|
|
167
|
+
expect((0, validation_1.validateCategory)('task')).toBe('task');
|
|
168
|
+
expect((0, validation_1.validateCategory)('decision')).toBe('decision');
|
|
169
|
+
expect((0, validation_1.validateCategory)('progress')).toBe('progress');
|
|
170
|
+
expect((0, validation_1.validateCategory)('note')).toBe('note');
|
|
171
|
+
expect((0, validation_1.validateCategory)('error')).toBe('error');
|
|
172
|
+
expect((0, validation_1.validateCategory)('warning')).toBe('warning');
|
|
173
|
+
expect((0, validation_1.validateCategory)('git')).toBe('git');
|
|
174
|
+
expect((0, validation_1.validateCategory)('system')).toBe('system');
|
|
175
|
+
});
|
|
176
|
+
it('should return undefined for empty category', () => {
|
|
177
|
+
expect((0, validation_1.validateCategory)('')).toBeUndefined();
|
|
178
|
+
expect((0, validation_1.validateCategory)(undefined)).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
it('should reject invalid categories', () => {
|
|
181
|
+
expect(() => (0, validation_1.validateCategory)('invalid')).toThrow(validation_1.ValidationError);
|
|
182
|
+
expect(() => (0, validation_1.validateCategory)('invalid')).toThrow('Invalid category');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('validatePriority', () => {
|
|
186
|
+
it('should accept valid priorities', () => {
|
|
187
|
+
expect((0, validation_1.validatePriority)('high')).toBe('high');
|
|
188
|
+
expect((0, validation_1.validatePriority)('normal')).toBe('normal');
|
|
189
|
+
expect((0, validation_1.validatePriority)('low')).toBe('low');
|
|
190
|
+
});
|
|
191
|
+
it('should default to normal for empty priority', () => {
|
|
192
|
+
expect((0, validation_1.validatePriority)('')).toBe('normal');
|
|
193
|
+
expect((0, validation_1.validatePriority)(undefined)).toBe('normal');
|
|
194
|
+
});
|
|
195
|
+
it('should reject invalid priorities', () => {
|
|
196
|
+
expect(() => (0, validation_1.validatePriority)('urgent')).toThrow(validation_1.ValidationError);
|
|
197
|
+
expect(() => (0, validation_1.validatePriority)('invalid')).toThrow('Invalid priority');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
const database_1 = require("../../utils/database");
|
|
37
|
+
const vector_store_1 = require("../../utils/vector-store");
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const uuid_1 = require("uuid");
|
|
42
|
+
describe('VectorStore', () => {
|
|
43
|
+
let dbManager;
|
|
44
|
+
let vectorStore;
|
|
45
|
+
let tempDbPath;
|
|
46
|
+
let db;
|
|
47
|
+
let testSessionId;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
tempDbPath = path.join(os.tmpdir(), `test-vector-${Date.now()}.db`);
|
|
50
|
+
dbManager = new database_1.DatabaseManager({
|
|
51
|
+
filename: tempDbPath,
|
|
52
|
+
maxSize: 10 * 1024 * 1024,
|
|
53
|
+
walMode: true,
|
|
54
|
+
});
|
|
55
|
+
db = dbManager.getDatabase();
|
|
56
|
+
vectorStore = new vector_store_1.VectorStore(db);
|
|
57
|
+
// Create test session and context items
|
|
58
|
+
testSessionId = (0, uuid_1.v4)();
|
|
59
|
+
db.prepare('INSERT INTO sessions (id, name, description) VALUES (?, ?, ?)').run(testSessionId, 'Test Session', 'Testing vector store');
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
dbManager.close();
|
|
63
|
+
try {
|
|
64
|
+
fs.unlinkSync(tempDbPath);
|
|
65
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
66
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
67
|
+
}
|
|
68
|
+
catch (_e) {
|
|
69
|
+
// Ignore
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
describe('Embedding creation', () => {
|
|
73
|
+
it('should create consistent embeddings for the same text', () => {
|
|
74
|
+
const text = 'This is a test sentence for embedding';
|
|
75
|
+
const embedding1 = vectorStore.createEmbedding(text);
|
|
76
|
+
const embedding2 = vectorStore.createEmbedding(text);
|
|
77
|
+
expect(embedding1).toEqual(embedding2);
|
|
78
|
+
expect(embedding1.length).toBe(384); // Default dimension
|
|
79
|
+
});
|
|
80
|
+
it('should create different embeddings for different text', () => {
|
|
81
|
+
const embedding1 = vectorStore.createEmbedding('First text');
|
|
82
|
+
const embedding2 = vectorStore.createEmbedding('Completely different text');
|
|
83
|
+
expect(embedding1).not.toEqual(embedding2);
|
|
84
|
+
});
|
|
85
|
+
it('should normalize embeddings', () => {
|
|
86
|
+
const embedding = vectorStore.createEmbedding('Test text');
|
|
87
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
|
|
88
|
+
expect(magnitude).toBeCloseTo(1.0, 5);
|
|
89
|
+
});
|
|
90
|
+
it('should handle empty text', () => {
|
|
91
|
+
const embedding = vectorStore.createEmbedding('');
|
|
92
|
+
expect(embedding.every(v => v === 0)).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('Document storage', () => {
|
|
96
|
+
it('should store a document with embedding', async () => {
|
|
97
|
+
const contentId = (0, uuid_1.v4)();
|
|
98
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(contentId, testSessionId, 'test_key', 'test value');
|
|
99
|
+
const docId = await vectorStore.storeDocument(contentId, 'test_key: test value', {
|
|
100
|
+
category: 'test',
|
|
101
|
+
});
|
|
102
|
+
expect(docId).toBeDefined();
|
|
103
|
+
// Verify in database
|
|
104
|
+
const stored = db.prepare('SELECT * FROM vector_embeddings WHERE id = ?').get(docId);
|
|
105
|
+
expect(stored).toBeDefined();
|
|
106
|
+
expect(stored.content).toBe('test_key: test value');
|
|
107
|
+
expect(stored.content_id).toBe(contentId);
|
|
108
|
+
expect(JSON.parse(stored.metadata)).toEqual({ category: 'test' });
|
|
109
|
+
});
|
|
110
|
+
it('should handle documents without metadata', async () => {
|
|
111
|
+
const contentId = (0, uuid_1.v4)();
|
|
112
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(contentId, testSessionId, 'test_key', 'test value');
|
|
113
|
+
const docId = await vectorStore.storeDocument(contentId, 'test content');
|
|
114
|
+
const stored = db.prepare('SELECT * FROM vector_embeddings WHERE id = ?').get(docId);
|
|
115
|
+
expect(stored.metadata).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('Semantic search', () => {
|
|
119
|
+
beforeEach(async () => {
|
|
120
|
+
// Create test context items with embeddings
|
|
121
|
+
const items = [
|
|
122
|
+
{ key: 'auth_task', value: 'Implement user authentication with JWT tokens' },
|
|
123
|
+
{ key: 'auth_decision', value: 'Use bcrypt for password hashing' },
|
|
124
|
+
{ key: 'db_task', value: 'Set up PostgreSQL database connection' },
|
|
125
|
+
{ key: 'api_task', value: 'Create REST API endpoints for user management' },
|
|
126
|
+
{ key: 'test_task', value: 'Write unit tests for authentication module' },
|
|
127
|
+
];
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
const itemId = (0, uuid_1.v4)();
|
|
130
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(itemId, testSessionId, item.key, item.value);
|
|
131
|
+
await vectorStore.storeDocument(itemId, `${item.key}: ${item.value}`, { key: item.key });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
it('should find relevant documents by semantic similarity', async () => {
|
|
135
|
+
const results = await vectorStore.search('authentication security', 5, 0.1);
|
|
136
|
+
expect(results.length).toBeGreaterThan(0);
|
|
137
|
+
// Authentication-related items should rank higher
|
|
138
|
+
const topResults = results.slice(0, 2);
|
|
139
|
+
const authRelated = topResults.filter(r => r.content.toLowerCase().includes('auth') || r.content.toLowerCase().includes('password'));
|
|
140
|
+
expect(authRelated.length).toBeGreaterThan(0);
|
|
141
|
+
});
|
|
142
|
+
it('should respect topK parameter', async () => {
|
|
143
|
+
const results = await vectorStore.search('task', 3);
|
|
144
|
+
expect(results.length).toBeLessThanOrEqual(3);
|
|
145
|
+
});
|
|
146
|
+
it('should filter by minimum similarity', async () => {
|
|
147
|
+
const results = await vectorStore.search('completely unrelated query xyz123', 10, 0.8);
|
|
148
|
+
expect(results.length).toBe(0);
|
|
149
|
+
});
|
|
150
|
+
it('should search within a specific session', async () => {
|
|
151
|
+
// Create another session with different content
|
|
152
|
+
const otherSessionId = (0, uuid_1.v4)();
|
|
153
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(otherSessionId, 'Other Session');
|
|
154
|
+
const itemId = (0, uuid_1.v4)();
|
|
155
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(itemId, otherSessionId, 'other_key', 'authentication in other session');
|
|
156
|
+
await vectorStore.storeDocument(itemId, 'other_key: authentication in other session');
|
|
157
|
+
// Search only in test session
|
|
158
|
+
const results = await vectorStore.searchInSession(testSessionId, 'authentication', 10, 0.1);
|
|
159
|
+
const otherSessionResults = results.filter(r => r.content.includes('other_key'));
|
|
160
|
+
expect(otherSessionResults.length).toBe(0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('Related documents', () => {
|
|
164
|
+
let docIds = [];
|
|
165
|
+
beforeEach(async () => {
|
|
166
|
+
const items = [
|
|
167
|
+
{ key: 'jwt_info', value: 'JWT tokens expire after 24 hours' },
|
|
168
|
+
{ key: 'jwt_impl', value: 'Implement JWT token generation and validation' },
|
|
169
|
+
{ key: 'session_info', value: 'Session management using Redis' },
|
|
170
|
+
];
|
|
171
|
+
for (const item of items) {
|
|
172
|
+
const itemId = (0, uuid_1.v4)();
|
|
173
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(itemId, testSessionId, item.key, item.value);
|
|
174
|
+
const docId = await vectorStore.storeDocument(itemId, `${item.key}: ${item.value}`);
|
|
175
|
+
docIds.push(docId);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
it('should find related documents', async () => {
|
|
179
|
+
const relatedToJwt = await vectorStore.findRelated(docIds[0], 5, 0.1);
|
|
180
|
+
expect(relatedToJwt.length).toBeGreaterThan(0);
|
|
181
|
+
// JWT implementation should be related to JWT info
|
|
182
|
+
const jwtImpl = relatedToJwt.find(r => r.content.includes('jwt_impl'));
|
|
183
|
+
expect(jwtImpl).toBeDefined();
|
|
184
|
+
});
|
|
185
|
+
it('should exclude the source document', async () => {
|
|
186
|
+
const related = await vectorStore.findRelated(docIds[0], 10, 0.0);
|
|
187
|
+
const self = related.find(r => r.id === docIds[0]);
|
|
188
|
+
expect(self).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe('Session embedding updates', () => {
|
|
192
|
+
it('should create embeddings for all context items in a session', async () => {
|
|
193
|
+
// Create context items without embeddings
|
|
194
|
+
const items = [
|
|
195
|
+
{ id: (0, uuid_1.v4)(), key: 'item1', value: 'First item' },
|
|
196
|
+
{ id: (0, uuid_1.v4)(), key: 'item2', value: 'Second item' },
|
|
197
|
+
{ id: (0, uuid_1.v4)(), key: 'item3', value: 'Third item' },
|
|
198
|
+
];
|
|
199
|
+
for (const item of items) {
|
|
200
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(item.id, testSessionId, item.key, item.value);
|
|
201
|
+
}
|
|
202
|
+
const count = await vectorStore.updateSessionEmbeddings(testSessionId);
|
|
203
|
+
expect(count).toBe(3);
|
|
204
|
+
// Verify embeddings were created
|
|
205
|
+
const embeddings = db
|
|
206
|
+
.prepare('SELECT COUNT(*) as count FROM vector_embeddings WHERE content_id IN (?, ?, ?)')
|
|
207
|
+
.get(items[0].id, items[1].id, items[2].id);
|
|
208
|
+
expect(embeddings.count).toBe(3);
|
|
209
|
+
});
|
|
210
|
+
it('should not duplicate embeddings', async () => {
|
|
211
|
+
const itemId = (0, uuid_1.v4)();
|
|
212
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(itemId, testSessionId, 'test', 'value');
|
|
213
|
+
// Create embedding
|
|
214
|
+
await vectorStore.storeDocument(itemId, 'test: value');
|
|
215
|
+
// Update should not create duplicate
|
|
216
|
+
const count = await vectorStore.updateSessionEmbeddings(testSessionId);
|
|
217
|
+
expect(count).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
describe('Statistics', () => {
|
|
221
|
+
it('should return document count', async () => {
|
|
222
|
+
const stats = vectorStore.getStats();
|
|
223
|
+
expect(stats.totalDocuments).toBe(0);
|
|
224
|
+
const itemId = (0, uuid_1.v4)();
|
|
225
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(itemId, testSessionId, 'test', 'value');
|
|
226
|
+
await vectorStore.storeDocument(itemId, 'test: value');
|
|
227
|
+
const newStats = vectorStore.getStats();
|
|
228
|
+
expect(newStats.totalDocuments).toBe(1);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleContextWatch = handleContextWatch;
|
|
4
|
+
exports.handleContextWatchCreate = handleContextWatchCreate;
|
|
5
|
+
exports.handleContextWatchPoll = handleContextWatchPoll;
|
|
6
|
+
exports.handleContextWatchStop = handleContextWatchStop;
|
|
7
|
+
exports.handleContextWatchList = handleContextWatchList;
|
|
8
|
+
const validation_js_1 = require("../utils/validation.js");
|
|
9
|
+
/**
|
|
10
|
+
* Validates filter parameters for watchers
|
|
11
|
+
*/
|
|
12
|
+
function validateFilters(filters) {
|
|
13
|
+
if (filters) {
|
|
14
|
+
// Validate keys
|
|
15
|
+
if (filters.keys !== undefined && !Array.isArray(filters.keys)) {
|
|
16
|
+
throw new validation_js_1.ValidationError('keys filter must be an array');
|
|
17
|
+
}
|
|
18
|
+
// Validate channels
|
|
19
|
+
if (filters.channels !== undefined && !Array.isArray(filters.channels)) {
|
|
20
|
+
throw new validation_js_1.ValidationError('channels filter must be an array');
|
|
21
|
+
}
|
|
22
|
+
// Validate categories
|
|
23
|
+
if (filters.categories !== undefined && !Array.isArray(filters.categories)) {
|
|
24
|
+
throw new validation_js_1.ValidationError('categories filter must be an array');
|
|
25
|
+
}
|
|
26
|
+
// Validate category values
|
|
27
|
+
if (filters.categories) {
|
|
28
|
+
const validCategories = ['task', 'decision', 'progress', 'note', 'error', 'warning'];
|
|
29
|
+
for (const cat of filters.categories) {
|
|
30
|
+
if (!validCategories.includes(cat)) {
|
|
31
|
+
throw new validation_js_1.ValidationError(`Invalid category: ${cat}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Validate priorities
|
|
36
|
+
if (filters.priorities !== undefined && !Array.isArray(filters.priorities)) {
|
|
37
|
+
throw new validation_js_1.ValidationError('priorities filter must be an array');
|
|
38
|
+
}
|
|
39
|
+
// Validate priority values
|
|
40
|
+
if (filters.priorities) {
|
|
41
|
+
const validPriorities = ['high', 'normal', 'low'];
|
|
42
|
+
for (const priority of filters.priorities) {
|
|
43
|
+
if (!validPriorities.includes(priority)) {
|
|
44
|
+
throw new validation_js_1.ValidationError(`Invalid priority: ${priority}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* context_watch handler - unified handler for all watch operations
|
|
52
|
+
*/
|
|
53
|
+
async function handleContextWatch(args, repositories, currentSessionId) {
|
|
54
|
+
const { action, watcherId, filters } = args;
|
|
55
|
+
try {
|
|
56
|
+
switch (action) {
|
|
57
|
+
case 'create': {
|
|
58
|
+
// Validate filters
|
|
59
|
+
validateFilters(filters);
|
|
60
|
+
// Create watcher
|
|
61
|
+
const watcher = repositories.watchers.createWatcher({
|
|
62
|
+
sessionId: currentSessionId,
|
|
63
|
+
filters: filters || {},
|
|
64
|
+
ttl: 1800, // 30 minutes default
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: JSON.stringify({
|
|
71
|
+
watcherId: watcher.id,
|
|
72
|
+
created: true,
|
|
73
|
+
filters: watcher.filters,
|
|
74
|
+
currentSequence: watcher.lastSequence,
|
|
75
|
+
expiresIn: '30 minutes',
|
|
76
|
+
}, null, 2),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
case 'poll': {
|
|
82
|
+
if (!watcherId) {
|
|
83
|
+
throw new validation_js_1.ValidationError('watcherId is required for poll action');
|
|
84
|
+
}
|
|
85
|
+
// Poll for changes
|
|
86
|
+
const result = repositories.watchers.pollChanges(watcherId, 100);
|
|
87
|
+
if (result.watcherStatus === 'deleted') {
|
|
88
|
+
throw new validation_js_1.ValidationError(`Watcher not found: ${watcherId}`);
|
|
89
|
+
}
|
|
90
|
+
if (result.watcherStatus === 'expired') {
|
|
91
|
+
// Check if it's actually stopped vs expired
|
|
92
|
+
const watcher = repositories.watchers.getWatcher(watcherId);
|
|
93
|
+
if (watcher && !watcher.isActive) {
|
|
94
|
+
throw new validation_js_1.ValidationError(`Watcher is stopped: ${watcherId}`);
|
|
95
|
+
}
|
|
96
|
+
throw new validation_js_1.ValidationError(`Watcher expired: ${watcherId}`);
|
|
97
|
+
}
|
|
98
|
+
// Transform changes to match test expectations
|
|
99
|
+
const transformedChanges = result.changes.map(change => ({
|
|
100
|
+
type: change.operation,
|
|
101
|
+
key: change.key,
|
|
102
|
+
value: change.operation !== 'DELETE' ? change.newValue : undefined,
|
|
103
|
+
category: change.category,
|
|
104
|
+
channel: change.channel,
|
|
105
|
+
sequence: change.sequenceId,
|
|
106
|
+
timestamp: change.createdAt,
|
|
107
|
+
}));
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: JSON.stringify({
|
|
113
|
+
watcherId,
|
|
114
|
+
changes: transformedChanges,
|
|
115
|
+
hasMore: result.hasMore,
|
|
116
|
+
lastSequence: result.lastSequence,
|
|
117
|
+
polledAt: new Date().toISOString(),
|
|
118
|
+
}, null, 2),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case 'stop': {
|
|
124
|
+
if (!watcherId) {
|
|
125
|
+
throw new validation_js_1.ValidationError('watcherId is required for stop action');
|
|
126
|
+
}
|
|
127
|
+
const stopped = repositories.watchers.stopWatcher(watcherId);
|
|
128
|
+
if (!stopped) {
|
|
129
|
+
throw new validation_js_1.ValidationError(`Watcher not found: ${watcherId}`);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: 'text',
|
|
135
|
+
text: JSON.stringify({
|
|
136
|
+
watcherId,
|
|
137
|
+
stopped: true,
|
|
138
|
+
}, null, 2),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
case 'list': {
|
|
144
|
+
const watchers = repositories.watchers.listWatchers(currentSessionId);
|
|
145
|
+
const watcherList = watchers.map(w => ({
|
|
146
|
+
watcherId: w.id,
|
|
147
|
+
active: w.isActive,
|
|
148
|
+
filters: w.filters,
|
|
149
|
+
lastSequence: w.lastSequence,
|
|
150
|
+
createdAt: w.createdAt,
|
|
151
|
+
expiresAt: w.expiresAt,
|
|
152
|
+
}));
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: 'text',
|
|
157
|
+
text: JSON.stringify({
|
|
158
|
+
watchers: watcherList,
|
|
159
|
+
total: watcherList.length,
|
|
160
|
+
}, null, 2),
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
throw new validation_js_1.ValidationError(`Unknown action: ${action}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof validation_js_1.ValidationError) {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: 'text',
|
|
175
|
+
text: `Error: ${error.message}`,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Context watch create handler
|
|
185
|
+
*/
|
|
186
|
+
async function handleContextWatchCreate(args, repositories, currentSessionId) {
|
|
187
|
+
return handleContextWatch({ ...args, action: 'create' }, repositories, currentSessionId);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Context watch poll handler
|
|
191
|
+
*/
|
|
192
|
+
async function handleContextWatchPoll(args, repositories, currentSessionId) {
|
|
193
|
+
return handleContextWatch({ ...args, action: 'poll' }, repositories, currentSessionId);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Context watch stop handler
|
|
197
|
+
*/
|
|
198
|
+
async function handleContextWatchStop(args, repositories, currentSessionId) {
|
|
199
|
+
return handleContextWatch({ ...args, action: 'stop' }, repositories, currentSessionId);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Context watch list handler
|
|
203
|
+
*/
|
|
204
|
+
async function handleContextWatchList(args, repositories, currentSessionId) {
|
|
205
|
+
return handleContextWatch({ ...args, action: 'list' }, repositories, currentSessionId);
|
|
206
|
+
}
|