mcp-memory-keeper 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1051 -0
  4. package/bin/mcp-memory-keeper +52 -0
  5. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  6. package/dist/__tests__/helpers/test-server.js +92 -0
  7. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  8. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  9. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  10. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  11. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  12. package/dist/__tests__/integration/channels.test.js +376 -0
  13. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  14. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  15. package/dist/__tests__/integration/context-operations.test.js +243 -0
  16. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  17. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  18. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  19. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  20. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  21. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  22. package/dist/__tests__/integration/contextSearch.test.js +938 -0
  23. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  24. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  25. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  26. package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
  27. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  28. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  29. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  30. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  31. package/dist/__tests__/integration/error-cases.test.js +407 -0
  32. package/dist/__tests__/integration/export-import.test.js +367 -0
  33. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  34. package/dist/__tests__/integration/file-operations.test.js +264 -0
  35. package/dist/__tests__/integration/git-integration.test.js +237 -0
  36. package/dist/__tests__/integration/index-tools.test.js +496 -0
  37. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  38. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  39. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  40. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  41. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  42. package/dist/__tests__/integration/migrations.test.js +528 -0
  43. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  44. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  45. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  46. package/dist/__tests__/integration/project-directory.test.js +283 -0
  47. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  48. package/dist/__tests__/integration/retention.test.js +513 -0
  49. package/dist/__tests__/integration/search.test.js +333 -0
  50. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  51. package/dist/__tests__/integration/server-initialization.test.js +307 -0
  52. package/dist/__tests__/integration/session-management.test.js +219 -0
  53. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  54. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  55. package/dist/__tests__/integration/summarization.test.js +308 -0
  56. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  57. package/dist/__tests__/security/input-validation.test.js +115 -0
  58. package/dist/__tests__/utils/agents.test.js +473 -0
  59. package/dist/__tests__/utils/database.test.js +177 -0
  60. package/dist/__tests__/utils/git.test.js +122 -0
  61. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  62. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  63. package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
  64. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  65. package/dist/__tests__/utils/validation.test.js +200 -0
  66. package/dist/__tests__/utils/vector-store.test.js +231 -0
  67. package/dist/handlers/contextWatchHandlers.js +206 -0
  68. package/dist/index.js +4310 -0
  69. package/dist/index.phase1.backup.js +410 -0
  70. package/dist/index.phase2.backup.js +704 -0
  71. package/dist/migrations/003_add_channels.js +174 -0
  72. package/dist/migrations/004_add_context_watch.js +151 -0
  73. package/dist/migrations/005_add_context_watch.js +98 -0
  74. package/dist/migrations/simplify-sharing.js +117 -0
  75. package/dist/repositories/BaseRepository.js +30 -0
  76. package/dist/repositories/CheckpointRepository.js +140 -0
  77. package/dist/repositories/ContextRepository.js +1873 -0
  78. package/dist/repositories/FileRepository.js +104 -0
  79. package/dist/repositories/RepositoryManager.js +62 -0
  80. package/dist/repositories/SessionRepository.js +66 -0
  81. package/dist/repositories/WatcherRepository.js +252 -0
  82. package/dist/repositories/index.js +15 -0
  83. package/dist/server.js +384 -0
  84. package/dist/test-helpers/database-helper.js +128 -0
  85. package/dist/types/entities.js +3 -0
  86. package/dist/utils/agents.js +791 -0
  87. package/dist/utils/channels.js +150 -0
  88. package/dist/utils/database.js +731 -0
  89. package/dist/utils/feature-flags.js +476 -0
  90. package/dist/utils/git.js +145 -0
  91. package/dist/utils/knowledge-graph.js +264 -0
  92. package/dist/utils/migrationHealthCheck.js +373 -0
  93. package/dist/utils/migrations.js +452 -0
  94. package/dist/utils/retention.js +460 -0
  95. package/dist/utils/timestamps.js +112 -0
  96. package/dist/utils/validation.js +296 -0
  97. package/dist/utils/vector-store.js +247 -0
  98. package/package.json +84 -0
@@ -0,0 +1,245 @@
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 globals_1 = require("@jest/globals");
37
+ const database_1 = require("../../utils/database");
38
+ const RepositoryManager_1 = require("../../repositories/RepositoryManager");
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ (0, globals_1.describe)('Backward Compatibility Tests', () => {
43
+ let dbManager;
44
+ let tempDbPath;
45
+ let repositories;
46
+ let testSessionId;
47
+ let _server;
48
+ // Mock handler function to test actual handler logic
49
+ async function callContextGet(args) {
50
+ // This simulates the actual handler logic from index.ts
51
+ const { key, category, channel, channels, sessionId: specificSessionId, includeMetadata, sort, limit, offset, createdAfter, createdBefore, keyPattern, priorities, } = args;
52
+ const targetSessionId = specificSessionId || testSessionId;
53
+ // Use enhanced query for complex queries or when we need pagination
54
+ if (sort !== undefined ||
55
+ limit !== undefined ||
56
+ offset ||
57
+ createdAfter ||
58
+ createdBefore ||
59
+ keyPattern ||
60
+ priorities ||
61
+ channel ||
62
+ channels ||
63
+ includeMetadata ||
64
+ (!key && !category) // If listing all items without filters, use pagination
65
+ ) {
66
+ const result = repositories.contexts.queryEnhanced({
67
+ sessionId: targetSessionId,
68
+ key,
69
+ category,
70
+ channel,
71
+ channels,
72
+ sort,
73
+ limit,
74
+ offset,
75
+ createdAfter,
76
+ createdBefore,
77
+ keyPattern,
78
+ priorities,
79
+ includeMetadata,
80
+ });
81
+ if (result.items.length === 0) {
82
+ return { content: [{ type: 'text', text: 'No matching context found' }] };
83
+ }
84
+ // Return enhanced format
85
+ const response = {
86
+ items: result.items,
87
+ pagination: {
88
+ total: result.totalCount,
89
+ returned: result.items.length,
90
+ offset: offset || 0,
91
+ hasMore: false, // Simplified for test
92
+ nextOffset: null,
93
+ },
94
+ };
95
+ return {
96
+ content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
97
+ };
98
+ }
99
+ // Backward compatible simple queries
100
+ let rows;
101
+ if (key) {
102
+ const item = repositories.contexts.getAccessibleByKey(targetSessionId, key);
103
+ rows = item ? [item] : [];
104
+ }
105
+ else {
106
+ rows = repositories.contexts.getAccessibleItems(targetSessionId, { category });
107
+ }
108
+ if (rows.length === 0) {
109
+ return { content: [{ type: 'text', text: 'No matching context found' }] };
110
+ }
111
+ if (key && rows.length === 1) {
112
+ // Single item requested - return just the value
113
+ const item = rows[0];
114
+ return { content: [{ type: 'text', text: item.value }] };
115
+ }
116
+ // Multiple items - return formatted list
117
+ const items = rows
118
+ .map((r) => `• [${r.priority}] ${r.key}: ${r.value.substring(0, 100)}${r.value.length > 100 ? '...' : ''}`)
119
+ .join('\n');
120
+ return {
121
+ content: [{ type: 'text', text: `Found ${rows.length} context items:\n\n${items}` }],
122
+ };
123
+ }
124
+ (0, globals_1.beforeEach)(() => {
125
+ tempDbPath = path.join(os.tmpdir(), `test-backward-compat-${Date.now()}.db`);
126
+ dbManager = new database_1.DatabaseManager({
127
+ filename: tempDbPath,
128
+ maxSize: 10 * 1024 * 1024,
129
+ walMode: true,
130
+ });
131
+ repositories = new RepositoryManager_1.RepositoryManager(dbManager);
132
+ // Create test session
133
+ const session = repositories.sessions.create({ name: 'Test Session' });
134
+ testSessionId = session.id;
135
+ // Create test data
136
+ repositories.contexts.save(testSessionId, {
137
+ key: 'single.item',
138
+ value: 'This is a single item value',
139
+ category: 'test',
140
+ priority: 'normal',
141
+ });
142
+ repositories.contexts.save(testSessionId, {
143
+ key: 'category.item1',
144
+ value: 'Category test item 1',
145
+ category: 'testcat',
146
+ priority: 'high',
147
+ });
148
+ repositories.contexts.save(testSessionId, {
149
+ key: 'category.item2',
150
+ value: 'Category test item 2',
151
+ category: 'testcat',
152
+ priority: 'normal',
153
+ });
154
+ // Create many items to test pagination
155
+ for (let i = 0; i < 150; i++) {
156
+ repositories.contexts.save(testSessionId, {
157
+ key: `bulk.item.${i.toString().padStart(3, '0')}`,
158
+ value: `Bulk item value ${i}`,
159
+ category: 'bulk',
160
+ priority: i % 2 === 0 ? 'high' : 'normal',
161
+ });
162
+ }
163
+ });
164
+ (0, globals_1.afterEach)(() => {
165
+ dbManager.close();
166
+ try {
167
+ fs.unlinkSync(tempDbPath);
168
+ fs.unlinkSync(`${tempDbPath}-wal`);
169
+ fs.unlinkSync(`${tempDbPath}-shm`);
170
+ }
171
+ catch (_e) {
172
+ // Ignore
173
+ }
174
+ });
175
+ (0, globals_1.describe)('Single Item Retrieval (Backward Compatible)', () => {
176
+ (0, globals_1.it)('should return just the value for single item by key', async () => {
177
+ const result = await callContextGet({ key: 'single.item' });
178
+ (0, globals_1.expect)(result.content[0].text).toBe('This is a single item value');
179
+ // Should NOT include pagination or JSON structure
180
+ (0, globals_1.expect)(result.content[0].text).not.toContain('pagination');
181
+ (0, globals_1.expect)(result.content[0].text).not.toContain('{');
182
+ });
183
+ (0, globals_1.it)('should return "No matching context found" for non-existent key', async () => {
184
+ const result = await callContextGet({ key: 'non.existent' });
185
+ (0, globals_1.expect)(result.content[0].text).toBe('No matching context found');
186
+ });
187
+ });
188
+ (0, globals_1.describe)('Category Filtering (Backward Compatible)', () => {
189
+ (0, globals_1.it)('should return formatted list for category filter only', async () => {
190
+ const result = await callContextGet({ category: 'testcat' });
191
+ (0, globals_1.expect)(result.content[0].text).toContain('Found 2 context items:');
192
+ (0, globals_1.expect)(result.content[0].text).toContain('• [high] category.item1:');
193
+ (0, globals_1.expect)(result.content[0].text).toContain('• [normal] category.item2:');
194
+ // Should NOT include pagination
195
+ (0, globals_1.expect)(result.content[0].text).not.toContain('pagination');
196
+ });
197
+ });
198
+ (0, globals_1.describe)('Enhanced Queries (New Format)', () => {
199
+ (0, globals_1.it)('should return paginated format when listing all items', async () => {
200
+ const result = await callContextGet({});
201
+ const parsed = JSON.parse(result.content[0].text);
202
+ (0, globals_1.expect)(parsed).toHaveProperty('items');
203
+ (0, globals_1.expect)(parsed).toHaveProperty('pagination');
204
+ (0, globals_1.expect)(parsed.pagination.total).toBe(153); // All test items
205
+ (0, globals_1.expect)(parsed.pagination.returned).toBe(100); // Default limit
206
+ });
207
+ (0, globals_1.it)('should return paginated format when using limit', async () => {
208
+ const result = await callContextGet({ category: 'bulk', limit: 10 });
209
+ const parsed = JSON.parse(result.content[0].text);
210
+ (0, globals_1.expect)(parsed).toHaveProperty('items');
211
+ (0, globals_1.expect)(parsed).toHaveProperty('pagination');
212
+ (0, globals_1.expect)(parsed.items).toHaveLength(10);
213
+ (0, globals_1.expect)(parsed.pagination.returned).toBe(10);
214
+ });
215
+ (0, globals_1.it)('should return paginated format when using sort', async () => {
216
+ const result = await callContextGet({ category: 'bulk', sort: 'key_asc' });
217
+ const parsed = JSON.parse(result.content[0].text);
218
+ (0, globals_1.expect)(parsed).toHaveProperty('items');
219
+ (0, globals_1.expect)(parsed).toHaveProperty('pagination');
220
+ (0, globals_1.expect)(parsed.items[0].key).toBe('bulk.item.000');
221
+ });
222
+ (0, globals_1.it)('should return paginated format when using includeMetadata', async () => {
223
+ const result = await callContextGet({ key: 'single.item', includeMetadata: true });
224
+ const parsed = JSON.parse(result.content[0].text);
225
+ (0, globals_1.expect)(parsed).toHaveProperty('items');
226
+ (0, globals_1.expect)(parsed).toHaveProperty('pagination');
227
+ (0, globals_1.expect)(parsed.items).toHaveLength(1);
228
+ (0, globals_1.expect)(parsed.items[0]).toHaveProperty('size');
229
+ (0, globals_1.expect)(parsed.items[0]).toHaveProperty('created_at');
230
+ });
231
+ });
232
+ (0, globals_1.describe)('Mixed Scenarios', () => {
233
+ (0, globals_1.it)('should use simple format for key+category filter', async () => {
234
+ // Even with both key and category, if key is specified, use simple format
235
+ const result = await callContextGet({ key: 'single.item', category: 'test' });
236
+ (0, globals_1.expect)(result.content[0].text).toBe('This is a single item value');
237
+ (0, globals_1.expect)(result.content[0].text).not.toContain('pagination');
238
+ });
239
+ (0, globals_1.it)('should use enhanced format when channel is specified', async () => {
240
+ const result = await callContextGet({ category: 'test', channel: 'general' });
241
+ const parsed = JSON.parse(result.content[0].text);
242
+ (0, globals_1.expect)(parsed).toHaveProperty('pagination');
243
+ });
244
+ });
245
+ });
@@ -0,0 +1,396 @@
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 globals_1 = require("@jest/globals");
37
+ // Removed unused imports: Server, CallToolRequestSchema
38
+ const database_1 = require("../../utils/database");
39
+ const RepositoryManager_1 = require("../../repositories/RepositoryManager");
40
+ const path = __importStar(require("path"));
41
+ const os = __importStar(require("os"));
42
+ const fs = __importStar(require("fs"));
43
+ (0, globals_1.describe)('Batch Operations E2E Tests', () => {
44
+ let tempDbPath;
45
+ let dbManager;
46
+ let repositories;
47
+ let testSessionId;
48
+ // Mock the server handler
49
+ const mockHandleToolCall = async (toolName, args) => {
50
+ // This simulates what happens in index.ts
51
+ switch (toolName) {
52
+ case 'context_batch_save': {
53
+ const { items, updateExisting = true } = args;
54
+ const sessionId = testSessionId;
55
+ // Validate items
56
+ if (!items || !Array.isArray(items) || items.length === 0) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: 'text',
61
+ text: 'No items provided for batch save',
62
+ },
63
+ ],
64
+ };
65
+ }
66
+ // Validate each item
67
+ const validationErrors = [];
68
+ items.forEach((item, index) => {
69
+ try {
70
+ if (!item.key || !item.key.trim()) {
71
+ throw new Error('Key is required and cannot be empty');
72
+ }
73
+ if (!item.value) {
74
+ throw new Error('Value is required');
75
+ }
76
+ if (item.category) {
77
+ const validCategories = ['task', 'decision', 'progress', 'note', 'error', 'warning'];
78
+ if (!validCategories.includes(item.category)) {
79
+ throw new Error(`Invalid category: ${item.category}`);
80
+ }
81
+ }
82
+ if (item.priority) {
83
+ const validPriorities = ['high', 'normal', 'low'];
84
+ if (!validPriorities.includes(item.priority)) {
85
+ throw new Error(`Invalid priority: ${item.priority}`);
86
+ }
87
+ }
88
+ }
89
+ catch (error) {
90
+ validationErrors.push({
91
+ index,
92
+ key: item.key || 'undefined',
93
+ error: error.message,
94
+ });
95
+ }
96
+ });
97
+ // Filter out items with validation errors before passing to repository
98
+ const validItemIndices = [];
99
+ const validItems = items.filter((_, index) => {
100
+ const hasError = validationErrors.some(err => err.index === index);
101
+ if (!hasError) {
102
+ validItemIndices.push(index);
103
+ return true;
104
+ }
105
+ return false;
106
+ });
107
+ if (validationErrors.length === items.length) {
108
+ return {
109
+ content: [
110
+ {
111
+ type: 'text',
112
+ text: JSON.stringify({
113
+ operation: 'batch_save',
114
+ totalItems: items.length,
115
+ succeeded: 0,
116
+ failed: validationErrors.length,
117
+ totalSize: 0,
118
+ results: [],
119
+ errors: validationErrors,
120
+ timestamp: new Date().toISOString(),
121
+ }, null, 2),
122
+ },
123
+ ],
124
+ };
125
+ }
126
+ let results = [];
127
+ let totalSize = 0;
128
+ dbManager.getDatabase().prepare('BEGIN TRANSACTION').run();
129
+ try {
130
+ // Track original indices
131
+ const indexMap = new Map();
132
+ validItems.forEach((item, newIdx) => {
133
+ indexMap.set(newIdx, items.indexOf(item));
134
+ });
135
+ const batchResult = repositories.contexts.batchSave(sessionId, validItems, {
136
+ updateExisting,
137
+ });
138
+ totalSize = batchResult.totalSize;
139
+ // Map results back to original indices
140
+ results = batchResult.results
141
+ .filter(r => r.success)
142
+ .map(r => ({
143
+ ...r,
144
+ index: indexMap.get(r.index) ?? r.index,
145
+ }));
146
+ const errors = [
147
+ ...validationErrors,
148
+ ...batchResult.results
149
+ .filter(r => !r.success)
150
+ .map(r => ({
151
+ index: indexMap.get(r.index) ?? r.index,
152
+ key: r.key,
153
+ error: r.error,
154
+ })),
155
+ ];
156
+ dbManager.getDatabase().prepare('COMMIT').run();
157
+ return {
158
+ content: [
159
+ {
160
+ type: 'text',
161
+ text: JSON.stringify({
162
+ operation: 'batch_save',
163
+ totalItems: items.length,
164
+ succeeded: results.length,
165
+ failed: errors.length,
166
+ totalSize: totalSize,
167
+ averageSize: results.length > 0 ? Math.round(totalSize / results.length) : 0,
168
+ results: results,
169
+ errors: errors,
170
+ timestamp: new Date().toISOString(),
171
+ }, null, 2),
172
+ },
173
+ ],
174
+ };
175
+ }
176
+ catch (error) {
177
+ dbManager.getDatabase().prepare('ROLLBACK').run();
178
+ return {
179
+ content: [
180
+ {
181
+ type: 'text',
182
+ text: `Batch save failed: ${error.message}`,
183
+ },
184
+ ],
185
+ };
186
+ }
187
+ }
188
+ case 'context_batch_delete': {
189
+ const { keys, keyPattern, dryRun = false } = args;
190
+ const sessionId = testSessionId;
191
+ if (dryRun) {
192
+ const itemsToDelete = repositories.contexts.getDryRunItems(sessionId, {
193
+ keys,
194
+ keyPattern,
195
+ });
196
+ return {
197
+ content: [
198
+ {
199
+ type: 'text',
200
+ text: JSON.stringify({
201
+ operation: 'batch_delete',
202
+ dryRun: true,
203
+ keys: keys,
204
+ pattern: keyPattern,
205
+ itemsToDelete: itemsToDelete,
206
+ totalItems: itemsToDelete.length,
207
+ }, null, 2),
208
+ },
209
+ ],
210
+ };
211
+ }
212
+ dbManager.getDatabase().prepare('BEGIN TRANSACTION').run();
213
+ try {
214
+ const deleteResult = repositories.contexts.batchDelete(sessionId, { keys, keyPattern });
215
+ dbManager.getDatabase().prepare('COMMIT').run();
216
+ const response = keys
217
+ ? {
218
+ operation: 'batch_delete',
219
+ keys: keys,
220
+ totalRequested: keys.length,
221
+ totalDeleted: deleteResult.totalDeleted,
222
+ notFound: deleteResult.results?.filter(r => !r.deleted).map(r => r.key) || [],
223
+ results: deleteResult.results,
224
+ }
225
+ : {
226
+ operation: 'batch_delete',
227
+ pattern: keyPattern,
228
+ totalDeleted: deleteResult.totalDeleted,
229
+ };
230
+ return {
231
+ content: [
232
+ {
233
+ type: 'text',
234
+ text: JSON.stringify(response, null, 2),
235
+ },
236
+ ],
237
+ };
238
+ }
239
+ catch (error) {
240
+ dbManager.getDatabase().prepare('ROLLBACK').run();
241
+ return {
242
+ content: [
243
+ {
244
+ type: 'text',
245
+ text: `Batch delete failed: ${error.message}`,
246
+ },
247
+ ],
248
+ };
249
+ }
250
+ }
251
+ case 'context_batch_update': {
252
+ const { updates } = args;
253
+ const sessionId = testSessionId;
254
+ dbManager.getDatabase().prepare('BEGIN TRANSACTION').run();
255
+ try {
256
+ const updateResult = repositories.contexts.batchUpdate(sessionId, updates);
257
+ dbManager.getDatabase().prepare('COMMIT').run();
258
+ const results = updateResult.results.filter(r => r.updated);
259
+ const errors = updateResult.results.filter(r => !r.updated);
260
+ return {
261
+ content: [
262
+ {
263
+ type: 'text',
264
+ text: JSON.stringify({
265
+ operation: 'batch_update',
266
+ totalItems: updates.length,
267
+ succeeded: results.length,
268
+ failed: errors.length,
269
+ results: results,
270
+ errors: errors,
271
+ }, null, 2),
272
+ },
273
+ ],
274
+ };
275
+ }
276
+ catch (error) {
277
+ dbManager.getDatabase().prepare('ROLLBACK').run();
278
+ return {
279
+ content: [
280
+ {
281
+ type: 'text',
282
+ text: `Batch update failed: ${error.message}`,
283
+ },
284
+ ],
285
+ };
286
+ }
287
+ }
288
+ default:
289
+ throw new Error(`Unknown tool: ${toolName}`);
290
+ }
291
+ };
292
+ (0, globals_1.beforeEach)(() => {
293
+ tempDbPath = path.join(os.tmpdir(), `test-batch-e2e-${Date.now()}.db`);
294
+ dbManager = new database_1.DatabaseManager({ filename: tempDbPath });
295
+ repositories = new RepositoryManager_1.RepositoryManager(dbManager);
296
+ // Create test session
297
+ const session = repositories.sessions.create({
298
+ name: 'E2E Test Session',
299
+ description: 'Testing batch operations E2E',
300
+ });
301
+ testSessionId = session.id;
302
+ });
303
+ (0, globals_1.afterEach)(() => {
304
+ dbManager.close();
305
+ try {
306
+ fs.unlinkSync(tempDbPath);
307
+ fs.unlinkSync(`${tempDbPath}-wal`);
308
+ fs.unlinkSync(`${tempDbPath}-shm`);
309
+ }
310
+ catch (_e) {
311
+ // Ignore
312
+ }
313
+ });
314
+ (0, globals_1.describe)('End-to-End Batch Operations', () => {
315
+ (0, globals_1.it)('should handle complete batch workflow', async () => {
316
+ // 1. Batch Save
317
+ const saveResponse = await mockHandleToolCall('context_batch_save', {
318
+ items: [
319
+ { key: 'user.name', value: 'John Doe', category: 'note', priority: 'normal' },
320
+ { key: 'user.email', value: 'john@example.com', category: 'note', priority: 'high' },
321
+ { key: 'app.version', value: '1.0.0', category: 'note', priority: 'low' },
322
+ ],
323
+ });
324
+ const saveResult = JSON.parse(saveResponse.content[0].text);
325
+ (0, globals_1.expect)(saveResult.operation).toBe('batch_save');
326
+ (0, globals_1.expect)(saveResult.succeeded).toBe(3);
327
+ (0, globals_1.expect)(saveResult.failed).toBe(0);
328
+ (0, globals_1.expect)(saveResult.totalSize).toBeGreaterThan(0);
329
+ // 2. Batch Update
330
+ const updateResponse = await mockHandleToolCall('context_batch_update', {
331
+ updates: [
332
+ { key: 'user.name', value: 'Jane Doe' },
333
+ { key: 'app.version', value: '2.0.0', priority: 'high' },
334
+ ],
335
+ });
336
+ const updateResult = JSON.parse(updateResponse.content[0].text);
337
+ (0, globals_1.expect)(updateResult.operation).toBe('batch_update');
338
+ (0, globals_1.expect)(updateResult.succeeded).toBe(2);
339
+ (0, globals_1.expect)(updateResult.failed).toBe(0);
340
+ // 3. Batch Delete (Dry Run)
341
+ const dryRunResponse = await mockHandleToolCall('context_batch_delete', {
342
+ keys: ['user.email', 'app.version'],
343
+ dryRun: true,
344
+ });
345
+ const dryRunResult = JSON.parse(dryRunResponse.content[0].text);
346
+ (0, globals_1.expect)(dryRunResult.dryRun).toBe(true);
347
+ (0, globals_1.expect)(dryRunResult.totalItems).toBe(2);
348
+ // 4. Batch Delete (Actual)
349
+ const deleteResponse = await mockHandleToolCall('context_batch_delete', {
350
+ keys: ['user.email', 'app.version'],
351
+ });
352
+ const deleteResult = JSON.parse(deleteResponse.content[0].text);
353
+ (0, globals_1.expect)(deleteResult.operation).toBe('batch_delete');
354
+ (0, globals_1.expect)(deleteResult.totalDeleted).toBe(2);
355
+ // Verify final state
356
+ const remaining = repositories.contexts.getBySessionId(testSessionId);
357
+ (0, globals_1.expect)(remaining.length).toBe(1);
358
+ (0, globals_1.expect)(remaining[0].key).toBe('user.name');
359
+ (0, globals_1.expect)(remaining[0].value).toBe('Jane Doe');
360
+ });
361
+ (0, globals_1.it)('should handle validation errors gracefully', async () => {
362
+ const response = await mockHandleToolCall('context_batch_save', {
363
+ items: [
364
+ { key: '', value: 'No key' },
365
+ { key: 'no.value' },
366
+ { key: 'valid.item', value: 'Valid' },
367
+ ],
368
+ });
369
+ const result = JSON.parse(response.content[0].text);
370
+ (0, globals_1.expect)(result.succeeded).toBe(1);
371
+ (0, globals_1.expect)(result.failed).toBe(2);
372
+ (0, globals_1.expect)(result.errors).toHaveLength(2);
373
+ });
374
+ (0, globals_1.it)('should handle pattern-based operations', async () => {
375
+ // Setup test data
376
+ await mockHandleToolCall('context_batch_save', {
377
+ items: [
378
+ { key: 'temp.file1', value: 'File 1' },
379
+ { key: 'temp.file2', value: 'File 2' },
380
+ { key: 'temp.cache', value: 'Cache' },
381
+ { key: 'keep.this', value: 'Keep' },
382
+ ],
383
+ });
384
+ // Delete by pattern
385
+ const response = await mockHandleToolCall('context_batch_delete', {
386
+ keyPattern: 'temp.*',
387
+ });
388
+ const result = JSON.parse(response.content[0].text);
389
+ (0, globals_1.expect)(result.totalDeleted).toBe(3);
390
+ // Verify only non-matching item remains
391
+ const remaining = repositories.contexts.getBySessionId(testSessionId);
392
+ (0, globals_1.expect)(remaining.length).toBe(1);
393
+ (0, globals_1.expect)(remaining[0].key).toBe('keep.this');
394
+ });
395
+ });
396
+ });