mcp-memory-keeper 0.10.2 → 0.12.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.
@@ -0,0 +1,374 @@
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 fs = __importStar(require("fs"));
37
+ const path = __importStar(require("path"));
38
+ const os = __importStar(require("os"));
39
+ const tool_profiles_1 = require("../../utils/tool-profiles");
40
+ describe('Tool Profiles', () => {
41
+ describe('ALL_TOOL_NAMES', () => {
42
+ it('should contain exactly 38 tool names', () => {
43
+ expect(tool_profiles_1.ALL_TOOL_NAMES).toHaveLength(38);
44
+ });
45
+ it('should have no duplicates', () => {
46
+ const unique = new Set(tool_profiles_1.ALL_TOOL_NAMES);
47
+ expect(unique.size).toBe(tool_profiles_1.ALL_TOOL_NAMES.length);
48
+ });
49
+ it('should all start with "context_"', () => {
50
+ for (const name of tool_profiles_1.ALL_TOOL_NAMES) {
51
+ expect(name).toMatch(/^context_/);
52
+ }
53
+ });
54
+ });
55
+ describe('ALL_TOOL_NAMES_SET', () => {
56
+ it('should be the same size as ALL_TOOL_NAMES', () => {
57
+ expect(tool_profiles_1.ALL_TOOL_NAMES_SET.size).toBe(tool_profiles_1.ALL_TOOL_NAMES.length);
58
+ });
59
+ it('should contain every entry from ALL_TOOL_NAMES', () => {
60
+ for (const name of tool_profiles_1.ALL_TOOL_NAMES) {
61
+ expect(tool_profiles_1.ALL_TOOL_NAMES_SET.has(name)).toBe(true);
62
+ }
63
+ });
64
+ });
65
+ describe('DEFAULT_PROFILES', () => {
66
+ it('should define minimal, standard, and full profiles', () => {
67
+ expect(tool_profiles_1.DEFAULT_PROFILES).toHaveProperty('minimal');
68
+ expect(tool_profiles_1.DEFAULT_PROFILES).toHaveProperty('standard');
69
+ expect(tool_profiles_1.DEFAULT_PROFILES).toHaveProperty('full');
70
+ });
71
+ it('minimal should have 8 tools', () => {
72
+ expect(tool_profiles_1.DEFAULT_PROFILES.minimal).toHaveLength(8);
73
+ });
74
+ it('standard should have 22 tools', () => {
75
+ expect(tool_profiles_1.DEFAULT_PROFILES.standard).toHaveLength(22);
76
+ });
77
+ it('full should have all 38 tools', () => {
78
+ expect(tool_profiles_1.DEFAULT_PROFILES.full).toHaveLength(38);
79
+ });
80
+ it('minimal should be a subset of standard', () => {
81
+ const standardSet = new Set(tool_profiles_1.DEFAULT_PROFILES.standard);
82
+ for (const tool of tool_profiles_1.DEFAULT_PROFILES.minimal) {
83
+ expect(standardSet.has(tool)).toBe(true);
84
+ }
85
+ });
86
+ it('standard should be a subset of full', () => {
87
+ const fullSet = new Set(tool_profiles_1.DEFAULT_PROFILES.full);
88
+ for (const tool of tool_profiles_1.DEFAULT_PROFILES.standard) {
89
+ expect(fullSet.has(tool)).toBe(true);
90
+ }
91
+ });
92
+ it('all tools in each profile should be valid tool names', () => {
93
+ for (const [_profileName, tools] of Object.entries(tool_profiles_1.DEFAULT_PROFILES)) {
94
+ for (const tool of tools) {
95
+ expect(tool_profiles_1.ALL_TOOL_NAMES_SET.has(tool)).toBe(true);
96
+ }
97
+ }
98
+ });
99
+ it('full profile should match ALL_TOOL_NAMES exactly', () => {
100
+ expect(new Set(tool_profiles_1.DEFAULT_PROFILES.full)).toEqual(new Set(tool_profiles_1.ALL_TOOL_NAMES));
101
+ });
102
+ });
103
+ describe('validateToolNames', () => {
104
+ it('should return empty array for all valid names', () => {
105
+ expect((0, tool_profiles_1.validateToolNames)(['context_save', 'context_get'])).toEqual([]);
106
+ });
107
+ it('should return unknown names', () => {
108
+ const result = (0, tool_profiles_1.validateToolNames)(['context_save', 'nonexistent_tool', 'another_fake']);
109
+ expect(result).toEqual(['nonexistent_tool', 'another_fake']);
110
+ });
111
+ it('should handle empty array', () => {
112
+ expect((0, tool_profiles_1.validateToolNames)([])).toEqual([]);
113
+ });
114
+ it('should handle all-invalid array', () => {
115
+ const result = (0, tool_profiles_1.validateToolNames)(['fake1', 'fake2']);
116
+ expect(result).toEqual(['fake1', 'fake2']);
117
+ });
118
+ });
119
+ describe('loadConfigFile', () => {
120
+ const tmpDir = path.join(os.tmpdir(), 'mcp-mk-tool-profiles-test');
121
+ beforeEach(() => {
122
+ if (!fs.existsSync(tmpDir)) {
123
+ fs.mkdirSync(tmpDir, { recursive: true });
124
+ }
125
+ });
126
+ afterEach(() => {
127
+ if (fs.existsSync(tmpDir)) {
128
+ fs.rmSync(tmpDir, { recursive: true, force: true });
129
+ }
130
+ });
131
+ it('should return null when config file does not exist', () => {
132
+ const result = (0, tool_profiles_1.loadConfigFile)(path.join(tmpDir, 'nonexistent.json'));
133
+ expect(result).toBeNull();
134
+ });
135
+ it('should parse valid config file', () => {
136
+ const configPath = path.join(tmpDir, 'config.json');
137
+ fs.writeFileSync(configPath, JSON.stringify({
138
+ profiles: {
139
+ minimal: ['context_save', 'context_get'],
140
+ },
141
+ }));
142
+ const result = (0, tool_profiles_1.loadConfigFile)(configPath);
143
+ expect(result).not.toBeNull();
144
+ expect(result.profiles.minimal).toEqual(['context_save', 'context_get']);
145
+ });
146
+ it('should return null and warn on JSON syntax error', () => {
147
+ const configPath = path.join(tmpDir, 'bad.json');
148
+ fs.writeFileSync(configPath, '{ invalid json }');
149
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
150
+ const result = (0, tool_profiles_1.loadConfigFile)(configPath);
151
+ expect(result).toBeNull();
152
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to load config file'));
153
+ warnSpy.mockRestore();
154
+ });
155
+ it('should return null and warn on missing profiles key', () => {
156
+ const configPath = path.join(tmpDir, 'no-profiles.json');
157
+ fs.writeFileSync(configPath, JSON.stringify({ something: 'else' }));
158
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
159
+ const result = (0, tool_profiles_1.loadConfigFile)(configPath);
160
+ expect(result).toBeNull();
161
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('missing a valid "profiles" key'));
162
+ warnSpy.mockRestore();
163
+ });
164
+ it('should handle config file with extra keys gracefully', () => {
165
+ const configPath = path.join(tmpDir, 'extra.json');
166
+ fs.writeFileSync(configPath, JSON.stringify({
167
+ profiles: { custom: ['context_save'] },
168
+ extraKey: 'ignored',
169
+ }));
170
+ const result = (0, tool_profiles_1.loadConfigFile)(configPath);
171
+ expect(result).not.toBeNull();
172
+ expect(result.profiles.custom).toEqual(['context_save']);
173
+ });
174
+ it('should return null when profiles is an array instead of object', () => {
175
+ const configPath = path.join(tmpDir, 'array-profiles.json');
176
+ fs.writeFileSync(configPath, JSON.stringify({ profiles: ['context_save'] }));
177
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
178
+ const result = (0, tool_profiles_1.loadConfigFile)(configPath);
179
+ expect(result).toBeNull();
180
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('missing a valid "profiles" key'));
181
+ warnSpy.mockRestore();
182
+ });
183
+ it('should return null when profiles is null', () => {
184
+ const configPath = path.join(tmpDir, 'null-profiles.json');
185
+ fs.writeFileSync(configPath, JSON.stringify({ profiles: null }));
186
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
187
+ const result = (0, tool_profiles_1.loadConfigFile)(configPath);
188
+ expect(result).toBeNull();
189
+ warnSpy.mockRestore();
190
+ });
191
+ it('should return null when root is an array', () => {
192
+ const configPath = path.join(tmpDir, 'root-array.json');
193
+ fs.writeFileSync(configPath, JSON.stringify([{ profiles: {} }]));
194
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
195
+ const result = (0, tool_profiles_1.loadConfigFile)(configPath);
196
+ expect(result).toBeNull();
197
+ warnSpy.mockRestore();
198
+ });
199
+ });
200
+ describe('resolveActiveProfile', () => {
201
+ const originalEnv = process.env.TOOL_PROFILE;
202
+ const tmpDir = path.join(os.tmpdir(), 'mcp-mk-resolve-profile-test');
203
+ beforeEach(() => {
204
+ delete process.env.TOOL_PROFILE;
205
+ if (!fs.existsSync(tmpDir)) {
206
+ fs.mkdirSync(tmpDir, { recursive: true });
207
+ }
208
+ });
209
+ afterEach(() => {
210
+ if (originalEnv !== undefined) {
211
+ process.env.TOOL_PROFILE = originalEnv;
212
+ }
213
+ else {
214
+ delete process.env.TOOL_PROFILE;
215
+ }
216
+ if (fs.existsSync(tmpDir)) {
217
+ fs.rmSync(tmpDir, { recursive: true, force: true });
218
+ }
219
+ });
220
+ it('should default to full when no env var and no config', () => {
221
+ const configPath = path.join(tmpDir, 'nonexistent.json');
222
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
223
+ expect(result.profileName).toBe('full');
224
+ expect(result.tools.size).toBe(38);
225
+ expect(result.source).toBe('default');
226
+ expect(result.warnings).toHaveLength(0);
227
+ });
228
+ it('should use env var profile from built-in defaults', () => {
229
+ process.env.TOOL_PROFILE = 'minimal';
230
+ const configPath = path.join(tmpDir, 'nonexistent.json');
231
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
232
+ expect(result.profileName).toBe('minimal');
233
+ expect(result.tools.size).toBe(8);
234
+ expect(result.source).toBe('env+builtin');
235
+ });
236
+ it('should use env var profile from config file (takes precedence)', () => {
237
+ process.env.TOOL_PROFILE = 'minimal';
238
+ const configPath = path.join(tmpDir, 'config.json');
239
+ // Config overrides minimal to have only 2 tools
240
+ fs.writeFileSync(configPath, JSON.stringify({
241
+ profiles: { minimal: ['context_save', 'context_get'] },
242
+ }));
243
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
244
+ expect(result.profileName).toBe('minimal');
245
+ expect(result.tools.size).toBe(2);
246
+ expect(result.source).toBe('env+config');
247
+ });
248
+ it('should warn and fallback to full on invalid profile name', () => {
249
+ process.env.TOOL_PROFILE = 'nonexistent_profile';
250
+ const configPath = path.join(tmpDir, 'nonexistent.json');
251
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
252
+ expect(result.profileName).toBe('full');
253
+ expect(result.tools.size).toBe(38);
254
+ expect(result.source).toBe('default');
255
+ expect(result.warnings.length).toBeGreaterThan(0);
256
+ expect(result.warnings[0]).toContain('Unknown TOOL_PROFILE');
257
+ });
258
+ it('should warn on unknown tool names in profile', () => {
259
+ const configPath = path.join(tmpDir, 'config.json');
260
+ fs.writeFileSync(configPath, JSON.stringify({
261
+ profiles: { custom: ['context_save', 'fake_tool'] },
262
+ }));
263
+ process.env.TOOL_PROFILE = 'custom';
264
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
265
+ expect(result.tools.size).toBe(1);
266
+ expect(result.tools.has('context_save')).toBe(true);
267
+ expect(result.warnings.length).toBeGreaterThan(0);
268
+ expect(result.warnings[0]).toContain('fake_tool');
269
+ });
270
+ it('should fallback to full when profile resolves to empty', () => {
271
+ const configPath = path.join(tmpDir, 'config.json');
272
+ fs.writeFileSync(configPath, JSON.stringify({
273
+ profiles: { empty: ['fake_tool_1', 'fake_tool_2'] },
274
+ }));
275
+ process.env.TOOL_PROFILE = 'empty';
276
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
277
+ expect(result.profileName).toBe('full');
278
+ expect(result.tools.size).toBe(38);
279
+ expect(result.warnings.some(w => w.includes('no valid tools'))).toBe(true);
280
+ });
281
+ it('should trim whitespace from TOOL_PROFILE env var', () => {
282
+ process.env.TOOL_PROFILE = ' minimal ';
283
+ const configPath = path.join(tmpDir, 'nonexistent.json');
284
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
285
+ expect(result.profileName).toBe('minimal');
286
+ expect(result.tools.size).toBe(8);
287
+ });
288
+ it('should support custom profiles from config file', () => {
289
+ const configPath = path.join(tmpDir, 'config.json');
290
+ fs.writeFileSync(configPath, JSON.stringify({
291
+ profiles: {
292
+ my_workflow: ['context_save', 'context_get', 'context_diff'],
293
+ },
294
+ }));
295
+ process.env.TOOL_PROFILE = 'my_workflow';
296
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
297
+ expect(result.profileName).toBe('my_workflow');
298
+ expect(result.tools.size).toBe(3);
299
+ expect(result.source).toBe('env+config');
300
+ });
301
+ it('should use standard profile from env var', () => {
302
+ process.env.TOOL_PROFILE = 'standard';
303
+ const configPath = path.join(tmpDir, 'nonexistent.json');
304
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
305
+ expect(result.profileName).toBe('standard');
306
+ expect(result.tools.size).toBe(22);
307
+ expect(result.source).toBe('env+builtin');
308
+ });
309
+ it('should treat empty string TOOL_PROFILE as unset', () => {
310
+ process.env.TOOL_PROFILE = '';
311
+ const configPath = path.join(tmpDir, 'nonexistent.json');
312
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
313
+ expect(result.profileName).toBe('full');
314
+ expect(result.tools.size).toBe(38);
315
+ expect(result.source).toBe('default');
316
+ });
317
+ it('should handle config profile with null value gracefully', () => {
318
+ const configPath = path.join(tmpDir, 'config.json');
319
+ fs.writeFileSync(configPath, JSON.stringify({ profiles: { broken: null } }));
320
+ process.env.TOOL_PROFILE = 'broken';
321
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
322
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
323
+ // 'broken' not in built-ins, so falls through to unknown profile fallback
324
+ expect(result.profileName).toBe('full');
325
+ expect(result.tools.size).toBe(38);
326
+ expect(result.warnings.some(w => w.includes('not a valid array of strings'))).toBe(true);
327
+ warnSpy.mockRestore();
328
+ });
329
+ it('should handle config profile with non-array value gracefully', () => {
330
+ const configPath = path.join(tmpDir, 'config.json');
331
+ fs.writeFileSync(configPath, JSON.stringify({ profiles: { broken: 'not-an-array' } }));
332
+ process.env.TOOL_PROFILE = 'broken';
333
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
334
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
335
+ expect(result.profileName).toBe('full');
336
+ expect(result.tools.size).toBe(38);
337
+ expect(result.warnings.some(w => w.includes('not a valid array of strings'))).toBe(true);
338
+ warnSpy.mockRestore();
339
+ });
340
+ it('should handle config profile with non-string array elements gracefully', () => {
341
+ const configPath = path.join(tmpDir, 'config.json');
342
+ fs.writeFileSync(configPath, JSON.stringify({ profiles: { broken: [1, null, true] } }));
343
+ process.env.TOOL_PROFILE = 'broken';
344
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
345
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
346
+ expect(result.profileName).toBe('full');
347
+ expect(result.tools.size).toBe(38);
348
+ expect(result.warnings.some(w => w.includes('not a valid array of strings'))).toBe(true);
349
+ warnSpy.mockRestore();
350
+ });
351
+ it('should fall back to built-in when config profile for known name is invalid', () => {
352
+ const configPath = path.join(tmpDir, 'config.json');
353
+ // Config has invalid 'minimal' entry, but built-in 'minimal' exists
354
+ fs.writeFileSync(configPath, JSON.stringify({ profiles: { minimal: 42 } }));
355
+ process.env.TOOL_PROFILE = 'minimal';
356
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
357
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
358
+ expect(result.profileName).toBe('minimal');
359
+ expect(result.tools.size).toBe(8);
360
+ expect(result.source).toBe('env+builtin');
361
+ expect(result.warnings.some(w => w.includes('not a valid array of strings'))).toBe(true);
362
+ warnSpy.mockRestore();
363
+ });
364
+ it('should deduplicate tool names in profile via Set', () => {
365
+ const configPath = path.join(tmpDir, 'config.json');
366
+ fs.writeFileSync(configPath, JSON.stringify({
367
+ profiles: { dupes: ['context_save', 'context_save', 'context_get'] },
368
+ }));
369
+ process.env.TOOL_PROFILE = 'dupes';
370
+ const result = (0, tool_profiles_1.resolveActiveProfile)(configPath);
371
+ expect(result.tools.size).toBe(2);
372
+ });
373
+ });
374
+ });