@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +542 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1281 -0
  4. package/bin/mcp-memory-keeper +54 -0
  5. package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
  6. package/dist/__tests__/e2e/server-e2e.test.js +341 -0
  7. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  8. package/dist/__tests__/helpers/test-server.js +92 -0
  9. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  10. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  11. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  12. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  13. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  14. package/dist/__tests__/integration/channels.test.js +376 -0
  15. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  16. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  17. package/dist/__tests__/integration/context-operations.test.js +243 -0
  18. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  19. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  20. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  21. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  22. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  23. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  24. package/dist/__tests__/integration/contextSearch.test.js +1054 -0
  25. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  26. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  27. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  28. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  29. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  30. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  31. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  32. package/dist/__tests__/integration/error-cases.test.js +411 -0
  33. package/dist/__tests__/integration/export-import.test.js +367 -0
  34. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  35. package/dist/__tests__/integration/file-operations.test.js +264 -0
  36. package/dist/__tests__/integration/filterBySessionId.test.js +251 -0
  37. package/dist/__tests__/integration/git-integration.test.js +241 -0
  38. package/dist/__tests__/integration/index-tools.test.js +496 -0
  39. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  40. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  41. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  42. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  43. package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
  44. package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
  45. package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
  46. package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
  47. package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -0
  48. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  49. package/dist/__tests__/integration/migrations.test.js +528 -0
  50. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  51. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  52. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  53. package/dist/__tests__/integration/project-directory.test.js +291 -0
  54. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  55. package/dist/__tests__/integration/retention.test.js +513 -0
  56. package/dist/__tests__/integration/search.test.js +333 -0
  57. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  58. package/dist/__tests__/integration/server-initialization.test.js +305 -0
  59. package/dist/__tests__/integration/session-management.test.js +219 -0
  60. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  61. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  62. package/dist/__tests__/integration/summarization.test.js +308 -0
  63. package/dist/__tests__/integration/tokenLimitEnforcement.test.js +134 -0
  64. package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
  65. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  66. package/dist/__tests__/security/input-validation.test.js +115 -0
  67. package/dist/__tests__/utils/agents.test.js +473 -0
  68. package/dist/__tests__/utils/database.test.js +177 -0
  69. package/dist/__tests__/utils/git.test.js +122 -0
  70. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  71. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  72. package/dist/__tests__/utils/project-directory-messages.test.js +192 -0
  73. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  74. package/dist/__tests__/utils/token-limits.test.js +225 -0
  75. package/dist/__tests__/utils/tool-profiles.test.js +374 -0
  76. package/dist/__tests__/utils/validation.test.js +200 -0
  77. package/dist/__tests__/utils/vector-store.test.js +231 -0
  78. package/dist/handlers/contextWatchHandlers.js +206 -0
  79. package/dist/index.js +4425 -0
  80. package/dist/migrations/003_add_channels.js +174 -0
  81. package/dist/migrations/004_add_context_watch.js +151 -0
  82. package/dist/migrations/005_add_context_watch.js +98 -0
  83. package/dist/migrations/simplify-sharing.js +117 -0
  84. package/dist/repositories/BaseRepository.js +30 -0
  85. package/dist/repositories/CheckpointRepository.js +140 -0
  86. package/dist/repositories/ContextRepository.js +2017 -0
  87. package/dist/repositories/FileRepository.js +104 -0
  88. package/dist/repositories/RepositoryManager.js +62 -0
  89. package/dist/repositories/SessionRepository.js +66 -0
  90. package/dist/repositories/WatcherRepository.js +252 -0
  91. package/dist/repositories/index.js +15 -0
  92. package/dist/test-helpers/database-helper.js +128 -0
  93. package/dist/types/entities.js +3 -0
  94. package/dist/utils/agents.js +791 -0
  95. package/dist/utils/channels.js +150 -0
  96. package/dist/utils/database.js +780 -0
  97. package/dist/utils/feature-flags.js +476 -0
  98. package/dist/utils/git.js +145 -0
  99. package/dist/utils/knowledge-graph.js +264 -0
  100. package/dist/utils/migrationHealthCheck.js +373 -0
  101. package/dist/utils/migrations.js +452 -0
  102. package/dist/utils/retention.js +460 -0
  103. package/dist/utils/timestamps.js +112 -0
  104. package/dist/utils/token-limits.js +350 -0
  105. package/dist/utils/tool-profiles.js +242 -0
  106. package/dist/utils/validation.js +296 -0
  107. package/dist/utils/vector-store.js +247 -0
  108. package/examples/config.json +31 -0
  109. package/examples/project-directory-setup.md +114 -0
  110. package/package.json +85 -0
@@ -0,0 +1,308 @@
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 os = __importStar(require("os"));
38
+ const path = __importStar(require("path"));
39
+ const fs = __importStar(require("fs"));
40
+ const uuid_1 = require("uuid");
41
+ describe('Summarization Integration Tests', () => {
42
+ let dbManager;
43
+ let tempDbPath;
44
+ let db;
45
+ let testSessionId;
46
+ beforeEach(() => {
47
+ tempDbPath = path.join(os.tmpdir(), `test-summary-${Date.now()}.db`);
48
+ dbManager = new database_1.DatabaseManager({
49
+ filename: tempDbPath,
50
+ maxSize: 10 * 1024 * 1024,
51
+ walMode: true,
52
+ });
53
+ db = dbManager.getDatabase();
54
+ // Create test session
55
+ testSessionId = (0, uuid_1.v4)();
56
+ db.prepare('INSERT INTO sessions (id, name, description) VALUES (?, ?, ?)').run(testSessionId, 'Summary Test Session', 'Testing summarization functionality');
57
+ });
58
+ afterEach(() => {
59
+ dbManager.close();
60
+ try {
61
+ fs.unlinkSync(tempDbPath);
62
+ fs.unlinkSync(`${tempDbPath}-wal`);
63
+ fs.unlinkSync(`${tempDbPath}-shm`);
64
+ }
65
+ catch (_e) {
66
+ // Ignore
67
+ }
68
+ });
69
+ describe('context_summarize', () => {
70
+ beforeEach(() => {
71
+ // Add diverse test data
72
+ const items = [
73
+ // Tasks
74
+ {
75
+ key: 'task_1',
76
+ value: 'Implement authentication system',
77
+ category: 'task',
78
+ priority: 'high',
79
+ },
80
+ { key: 'task_2', value: 'Add rate limiting to API', category: 'task', priority: 'high' },
81
+ {
82
+ key: 'task_3',
83
+ value: 'Write unit tests for auth module',
84
+ category: 'task',
85
+ priority: 'normal',
86
+ },
87
+ { key: 'task_4', value: 'Update documentation', category: 'task', priority: 'low' },
88
+ // Decisions
89
+ {
90
+ key: 'decision_1',
91
+ value: 'Use JWT for authentication tokens',
92
+ category: 'decision',
93
+ priority: 'high',
94
+ },
95
+ {
96
+ key: 'decision_2',
97
+ value: 'Set token expiry to 24 hours',
98
+ category: 'decision',
99
+ priority: 'normal',
100
+ },
101
+ // Progress
102
+ {
103
+ key: 'progress_1',
104
+ value: 'Completed login endpoint',
105
+ category: 'progress',
106
+ priority: 'normal',
107
+ },
108
+ { key: 'progress_2', value: 'Fixed CORS issues', category: 'progress', priority: 'normal' },
109
+ // Notes
110
+ {
111
+ key: 'note_1',
112
+ value: 'Redis connection string: redis://localhost:6379',
113
+ category: 'note',
114
+ priority: 'normal',
115
+ },
116
+ {
117
+ key: 'note_2',
118
+ value: 'API rate limit: 100 requests per minute',
119
+ category: 'note',
120
+ priority: 'normal',
121
+ },
122
+ // Warnings
123
+ {
124
+ key: 'warning_1',
125
+ value: 'Deprecation warning in auth library',
126
+ category: 'warning',
127
+ priority: 'high',
128
+ },
129
+ ];
130
+ items.forEach(item => {
131
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority);
132
+ });
133
+ });
134
+ it('should summarize all items in session', () => {
135
+ const items = db
136
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY priority DESC, created_at DESC')
137
+ .all(testSessionId);
138
+ expect(items).toHaveLength(11);
139
+ // Group by category
140
+ const grouped = items.reduce((acc, item) => {
141
+ if (!acc[item.category])
142
+ acc[item.category] = [];
143
+ acc[item.category].push(item);
144
+ return acc;
145
+ }, {});
146
+ expect(Object.keys(grouped)).toHaveLength(5);
147
+ expect(grouped.task).toHaveLength(4);
148
+ expect(grouped.decision).toHaveLength(2);
149
+ expect(grouped.progress).toHaveLength(2);
150
+ expect(grouped.note).toHaveLength(2);
151
+ expect(grouped.warning).toHaveLength(1);
152
+ });
153
+ it('should filter by categories', () => {
154
+ const categories = ['task', 'decision'];
155
+ const items = db
156
+ .prepare(`SELECT * FROM context_items
157
+ WHERE session_id = ?
158
+ AND category IN (${categories.map(() => '?').join(',')})
159
+ ORDER BY priority DESC, created_at DESC`)
160
+ .all(testSessionId, ...categories);
161
+ expect(items).toHaveLength(6); // 4 tasks + 2 decisions
162
+ expect(items.every((i) => categories.includes(i.category))).toBe(true);
163
+ });
164
+ it('should limit summary length', () => {
165
+ // Add many items
166
+ for (let i = 0; i < 50; i++) {
167
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `extra_${i}`, `Extra item ${i}`, 'note');
168
+ }
169
+ // Get limited summary
170
+ const limit = 20;
171
+ const items = db
172
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY priority DESC, created_at DESC LIMIT ?')
173
+ .all(testSessionId, limit);
174
+ expect(items).toHaveLength(limit);
175
+ });
176
+ it('should prioritize high priority items', () => {
177
+ const items = db
178
+ .prepare(`SELECT * FROM context_items
179
+ WHERE session_id = ?
180
+ ORDER BY
181
+ CASE priority
182
+ WHEN 'critical' THEN 1
183
+ WHEN 'high' THEN 2
184
+ WHEN 'normal' THEN 3
185
+ WHEN 'low' THEN 4
186
+ END,
187
+ created_at DESC`)
188
+ .all(testSessionId);
189
+ // Check that high priority items come first (after critical if any)
190
+ const highPriorityItems = items.filter((i) => i.priority === 'high');
191
+ const firstHighIndex = items.findIndex((i) => i.priority === 'high');
192
+ // Find last high index manually (findLastIndex not available in older Node)
193
+ let lastHighIndex = -1;
194
+ for (let i = items.length - 1; i >= 0; i--) {
195
+ if (items[i].priority === 'high') {
196
+ lastHighIndex = i;
197
+ break;
198
+ }
199
+ }
200
+ if (highPriorityItems.length > 0) {
201
+ // All high priority items should be consecutive
202
+ const expectedLastIndex = firstHighIndex + highPriorityItems.length - 1;
203
+ expect(lastHighIndex).toBe(expectedLastIndex);
204
+ }
205
+ // Verify we have the expected high priority items
206
+ expect(highPriorityItems).toHaveLength(4); // 2 tasks + 1 decision + 1 warning
207
+ });
208
+ it('should generate summary with statistics', () => {
209
+ // Get statistics
210
+ const stats = db
211
+ .prepare(`SELECT
212
+ category,
213
+ priority,
214
+ COUNT(*) as count
215
+ FROM context_items
216
+ WHERE session_id = ?
217
+ GROUP BY category, priority`)
218
+ .all(testSessionId);
219
+ // Verify statistics
220
+ const taskHighCount = stats.find((s) => s.category === 'task' && s.priority === 'high')?.count;
221
+ expect(taskHighCount).toBe(2);
222
+ const totalCount = stats.reduce((sum, s) => sum + s.count, 0);
223
+ expect(totalCount).toBe(11);
224
+ });
225
+ it('should include session metadata in summary', () => {
226
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(testSessionId);
227
+ expect(session.name).toBe('Summary Test Session');
228
+ expect(session.description).toBe('Testing summarization functionality');
229
+ expect(session.created_at).toBeDefined();
230
+ });
231
+ it('should handle empty sessions', () => {
232
+ // Create empty session
233
+ const emptySessionId = (0, uuid_1.v4)();
234
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(emptySessionId, 'Empty Session');
235
+ const items = db
236
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
237
+ .all(emptySessionId);
238
+ expect(items).toHaveLength(0);
239
+ });
240
+ it('should generate category-specific summaries', () => {
241
+ // Tasks summary
242
+ const tasks = db
243
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ? ORDER BY priority DESC')
244
+ .all(testSessionId, 'task');
245
+ const taskSummary = {
246
+ total: tasks.length,
247
+ high: tasks.filter((t) => t.priority === 'high').length,
248
+ normal: tasks.filter((t) => t.priority === 'normal').length,
249
+ low: tasks.filter((t) => t.priority === 'low').length,
250
+ items: tasks.slice(0, 5), // Top 5 tasks
251
+ };
252
+ expect(taskSummary.total).toBe(4);
253
+ expect(taskSummary.high).toBe(2);
254
+ expect(taskSummary.normal).toBe(1);
255
+ expect(taskSummary.low).toBe(1);
256
+ });
257
+ it('should format summary for AI consumption', () => {
258
+ const items = db
259
+ .prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY category, priority DESC')
260
+ .all(testSessionId);
261
+ // Format for AI
262
+ const summary = [];
263
+ let currentCategory = '';
264
+ items.forEach((item) => {
265
+ if (item.category !== currentCategory) {
266
+ currentCategory = item.category;
267
+ summary.push(`\n## ${currentCategory.toUpperCase()}`);
268
+ }
269
+ const priorityMarker = item.priority === 'high' ? '🔴' : item.priority === 'normal' ? '🟡' : '⚪';
270
+ summary.push(`${priorityMarker} ${item.key}: ${item.value}`);
271
+ });
272
+ const formattedSummary = summary.join('\n');
273
+ expect(formattedSummary).toContain('## TASK');
274
+ expect(formattedSummary).toContain('## DECISION');
275
+ expect(formattedSummary).toContain('🔴'); // High priority markers
276
+ });
277
+ it('should include temporal information in summary', () => {
278
+ // Get items with time info
279
+ const items = db
280
+ .prepare(`SELECT
281
+ *,
282
+ strftime('%Y-%m-%d %H:%M', created_at) as formatted_time
283
+ FROM context_items
284
+ WHERE session_id = ?
285
+ ORDER BY created_at DESC
286
+ LIMIT 5`)
287
+ .all(testSessionId);
288
+ expect(items).toHaveLength(5);
289
+ items.forEach((item) => {
290
+ expect(item.formatted_time).toBeDefined();
291
+ expect(item.formatted_time).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/);
292
+ });
293
+ });
294
+ });
295
+ describe('Summary with file information', () => {
296
+ it('should include file cache statistics', () => {
297
+ // Add some files
298
+ for (let i = 0; i < 5; i++) {
299
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `/src/file${i}.ts`, `content${i}`, `hash${i}`);
300
+ }
301
+ const fileStats = db
302
+ .prepare('SELECT COUNT(*) as count, SUM(LENGTH(content)) as total_size FROM file_cache WHERE session_id = ?')
303
+ .get(testSessionId);
304
+ expect(fileStats.count).toBe(5);
305
+ expect(fileStats.total_size).toBeGreaterThan(0);
306
+ });
307
+ });
308
+ });
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ // Helper functions from the main index.ts file
5
+ function estimateTokens(text) {
6
+ return Math.ceil(text.length / 4);
7
+ }
8
+ function calculateSafeItemCount(items, tokenLimit) {
9
+ if (items.length === 0)
10
+ return 0;
11
+ let safeCount = 0;
12
+ let currentTokens = 0;
13
+ // Include base response structure in token calculation
14
+ const baseResponse = {
15
+ items: [],
16
+ pagination: {
17
+ total: 0,
18
+ returned: 0,
19
+ offset: 0,
20
+ hasMore: false,
21
+ nextOffset: null,
22
+ totalCount: 0,
23
+ page: 1,
24
+ pageSize: 0,
25
+ totalPages: 1,
26
+ hasNextPage: false,
27
+ hasPreviousPage: false,
28
+ previousOffset: null,
29
+ totalSize: 0,
30
+ averageSize: 0,
31
+ defaultsApplied: {},
32
+ truncated: false,
33
+ truncatedCount: 0,
34
+ },
35
+ };
36
+ // Estimate tokens for base response structure
37
+ const baseTokens = estimateTokens(JSON.stringify(baseResponse, null, 2));
38
+ currentTokens = baseTokens;
39
+ // Add items one by one until we approach the token limit
40
+ for (let i = 0; i < items.length; i++) {
41
+ const itemTokens = estimateTokens(JSON.stringify(items[i], null, 2));
42
+ // Leave some buffer (10%) to account for formatting and additional metadata
43
+ if (currentTokens + itemTokens > tokenLimit * 0.9) {
44
+ break;
45
+ }
46
+ currentTokens += itemTokens;
47
+ safeCount++;
48
+ }
49
+ // Always return at least 1 item if any exist, even if it exceeds limit
50
+ // This prevents infinite loops and ensures progress
51
+ return Math.max(safeCount, items.length > 0 ? 1 : 0);
52
+ }
53
+ (0, globals_1.describe)('Token Limit Enforcement Unit Tests', () => {
54
+ (0, globals_1.describe)('calculateSafeItemCount', () => {
55
+ (0, globals_1.it)('should return 0 for empty items array', () => {
56
+ const result = calculateSafeItemCount([], 20000);
57
+ (0, globals_1.expect)(result).toBe(0);
58
+ });
59
+ (0, globals_1.it)('should return at least 1 item if any exist', () => {
60
+ const largeItem = {
61
+ key: 'large.item',
62
+ value: 'X'.repeat(100000), // Very large item
63
+ category: 'test',
64
+ priority: 'high',
65
+ };
66
+ const result = calculateSafeItemCount([largeItem], 20000);
67
+ (0, globals_1.expect)(result).toBe(1);
68
+ });
69
+ (0, globals_1.it)('should truncate items when approaching token limit', () => {
70
+ // Create multiple medium-sized items
71
+ const items = [];
72
+ for (let i = 0; i < 50; i++) {
73
+ items.push({
74
+ key: `item.${i}`,
75
+ value: 'This is a medium-sized test value that contains enough text to trigger token limit enforcement when many items are returned together. '.repeat(20),
76
+ category: 'test',
77
+ priority: 'high',
78
+ });
79
+ }
80
+ const result = calculateSafeItemCount(items, 20000);
81
+ (0, globals_1.expect)(result).toBeLessThan(50);
82
+ (0, globals_1.expect)(result).toBeGreaterThan(0);
83
+ });
84
+ (0, globals_1.it)('should handle small items that all fit within limit', () => {
85
+ const items = [];
86
+ for (let i = 0; i < 10; i++) {
87
+ items.push({
88
+ key: `small.item.${i}`,
89
+ value: 'Small value',
90
+ category: 'test',
91
+ priority: 'high',
92
+ });
93
+ }
94
+ const result = calculateSafeItemCount(items, 20000);
95
+ (0, globals_1.expect)(result).toBe(10);
96
+ });
97
+ (0, globals_1.it)('should respect token limit with buffer', () => {
98
+ // Create items that would exceed token limit
99
+ const items = [];
100
+ const itemValue = 'X'.repeat(2000); // 2KB item that will definitely cause truncation
101
+ for (let i = 0; i < 100; i++) {
102
+ items.push({
103
+ key: `large.buffer.item.${i}`,
104
+ value: itemValue,
105
+ category: 'test',
106
+ priority: 'high',
107
+ });
108
+ }
109
+ const result = calculateSafeItemCount(items, 20000);
110
+ // Should be significantly less than all items due to token limits
111
+ (0, globals_1.expect)(result).toBeLessThan(100);
112
+ (0, globals_1.expect)(result).toBeGreaterThan(0);
113
+ // Verify that the result respects the buffer by checking actual tokens
114
+ const actualTokens = result * estimateTokens(JSON.stringify(items[0], null, 2));
115
+ (0, globals_1.expect)(actualTokens).toBeLessThan(20000 * 0.9); // Should be under 90% of limit
116
+ });
117
+ });
118
+ (0, globals_1.describe)('estimateTokens', () => {
119
+ (0, globals_1.it)('should estimate tokens correctly', () => {
120
+ const text = 'This is a test string';
121
+ const tokens = estimateTokens(text);
122
+ (0, globals_1.expect)(tokens).toBe(Math.ceil(text.length / 4));
123
+ });
124
+ (0, globals_1.it)('should handle empty strings', () => {
125
+ const tokens = estimateTokens('');
126
+ (0, globals_1.expect)(tokens).toBe(0);
127
+ });
128
+ (0, globals_1.it)('should handle large strings', () => {
129
+ const largeText = 'X'.repeat(10000);
130
+ const tokens = estimateTokens(largeText);
131
+ (0, globals_1.expect)(tokens).toBe(2500); // 10000 / 4
132
+ });
133
+ });
134
+ });
@@ -0,0 +1,150 @@
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 fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const tool_profiles_1 = require("../../utils/tool-profiles");
40
+ /**
41
+ * Drift-detection: extract tool names from the ListToolsRequestSchema handler
42
+ * in src/index.ts to verify ALL_TOOL_NAMES stays in sync with actual tool definitions.
43
+ */
44
+ function extractToolNamesFromIndexTs() {
45
+ const indexPath = path.join(__dirname, '..', '..', 'index.ts');
46
+ const src = fs.readFileSync(indexPath, 'utf-8');
47
+ // Find the allTools array: starts after "const allTools" and ends at the matching "];"
48
+ // We look for tool name strings inside the ListToolsRequestSchema handler
49
+ const toolNameRegex = /^\s+name:\s+'(context_[a-z_]+)'/gm;
50
+ const names = [];
51
+ let match;
52
+ // Only capture tool names outside of block comments (skip commented-out tools)
53
+ // Split by block comment boundaries and only scan non-comment sections
54
+ const sections = src.split(/\/\*[\s\S]*?\*\//);
55
+ for (const section of sections) {
56
+ toolNameRegex.lastIndex = 0;
57
+ while ((match = toolNameRegex.exec(section)) !== null) {
58
+ names.push(match[1]);
59
+ }
60
+ }
61
+ return names;
62
+ }
63
+ (0, globals_1.describe)('Tool Profile Integration Tests', () => {
64
+ const originalEnv = process.env.TOOL_PROFILE;
65
+ afterEach(() => {
66
+ if (originalEnv !== undefined) {
67
+ process.env.TOOL_PROFILE = originalEnv;
68
+ }
69
+ else {
70
+ delete process.env.TOOL_PROFILE;
71
+ }
72
+ });
73
+ (0, globals_1.describe)('Drift detection: ALL_TOOL_NAMES vs index.ts', () => {
74
+ (0, globals_1.it)('ALL_TOOL_NAMES should match active tools defined in index.ts', () => {
75
+ const indexToolNames = extractToolNamesFromIndexTs();
76
+ const allToolNamesArray = [...tool_profiles_1.ALL_TOOL_NAMES];
77
+ // Same count
78
+ (0, globals_1.expect)(allToolNamesArray.length).toBe(indexToolNames.length);
79
+ // Same set of names
80
+ (0, globals_1.expect)(new Set(allToolNamesArray)).toEqual(new Set(indexToolNames));
81
+ });
82
+ (0, globals_1.it)('should not include commented-out tools', () => {
83
+ // context_share and context_get_shared are commented out in index.ts
84
+ (0, globals_1.expect)(tool_profiles_1.ALL_TOOL_NAMES_SET.has('context_share')).toBe(false);
85
+ (0, globals_1.expect)(tool_profiles_1.ALL_TOOL_NAMES_SET.has('context_get_shared')).toBe(false);
86
+ });
87
+ });
88
+ (0, globals_1.describe)('Profile filtering behavior', () => {
89
+ (0, globals_1.it)('minimal profile should include core tools and exclude advanced tools', () => {
90
+ process.env.TOOL_PROFILE = 'minimal';
91
+ const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
92
+ // Core tools present
93
+ (0, globals_1.expect)(profile.tools.has('context_save')).toBe(true);
94
+ (0, globals_1.expect)(profile.tools.has('context_get')).toBe(true);
95
+ (0, globals_1.expect)(profile.tools.has('context_search')).toBe(true);
96
+ (0, globals_1.expect)(profile.tools.has('context_checkpoint')).toBe(true);
97
+ // Advanced tools absent
98
+ (0, globals_1.expect)(profile.tools.has('context_analyze')).toBe(false);
99
+ (0, globals_1.expect)(profile.tools.has('context_visualize')).toBe(false);
100
+ (0, globals_1.expect)(profile.tools.has('context_delegate')).toBe(false);
101
+ (0, globals_1.expect)(profile.tools.has('context_semantic_search')).toBe(false);
102
+ });
103
+ (0, globals_1.it)('default (no env var) should expose all tools with backwards-compatible behavior', () => {
104
+ delete process.env.TOOL_PROFILE;
105
+ const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
106
+ (0, globals_1.expect)(profile.tools.size).toBe(tool_profiles_1.ALL_TOOL_NAMES.length);
107
+ (0, globals_1.expect)(profile.profileName).toBe('full');
108
+ (0, globals_1.expect)(profile.source).toBe('default');
109
+ (0, globals_1.expect)(profile.warnings).toHaveLength(0);
110
+ });
111
+ });
112
+ (0, globals_1.describe)('CallTool guard behavior', () => {
113
+ (0, globals_1.it)('disabled tool should be in ALL_TOOL_NAMES_SET but not in profile tools', () => {
114
+ process.env.TOOL_PROFILE = 'minimal';
115
+ const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
116
+ const disabledTool = 'context_analyze';
117
+ // The guard logic: known tool that is not enabled
118
+ const isKnown = tool_profiles_1.ALL_TOOL_NAMES_SET.has(disabledTool);
119
+ const isEnabled = profile.tools.has(disabledTool);
120
+ (0, globals_1.expect)(isKnown).toBe(true);
121
+ (0, globals_1.expect)(isEnabled).toBe(false);
122
+ // In index.ts: isKnown && !isEnabled → return isError: true
123
+ });
124
+ (0, globals_1.it)('unknown tool should not be in ALL_TOOL_NAMES_SET (falls through to default switch)', () => {
125
+ const unknownTool = 'non_existent_tool';
126
+ (0, globals_1.expect)(tool_profiles_1.ALL_TOOL_NAMES_SET.has(unknownTool)).toBe(false);
127
+ // In index.ts: !isKnown → falls through to default: throw new Error()
128
+ });
129
+ (0, globals_1.it)('enabled tool should pass both checks', () => {
130
+ process.env.TOOL_PROFILE = 'minimal';
131
+ const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
132
+ const enabledTool = 'context_save';
133
+ const isKnown = tool_profiles_1.ALL_TOOL_NAMES_SET.has(enabledTool);
134
+ const isEnabled = profile.tools.has(enabledTool);
135
+ (0, globals_1.expect)(isKnown).toBe(true);
136
+ (0, globals_1.expect)(isEnabled).toBe(true);
137
+ // In index.ts: isKnown && isEnabled → guard does not fire, proceeds to switch
138
+ });
139
+ });
140
+ (0, globals_1.describe)('TOOL_PROFILE_CONFIG support', () => {
141
+ (0, globals_1.it)('resolveActiveProfile accepts custom config path (used by TOOL_PROFILE_CONFIG)', () => {
142
+ // This tests the mechanism that index.ts uses:
143
+ // resolveActiveProfile(process.env.TOOL_PROFILE_CONFIG)
144
+ const result = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/custom/path/config.json');
145
+ // Missing file → no config → falls back to built-in 'full'
146
+ (0, globals_1.expect)(result.profileName).toBe('full');
147
+ (0, globals_1.expect)(result.tools.size).toBe(38);
148
+ });
149
+ });
150
+ });